GUI.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import platform
  2. import sys
  3. import customtkinter as ctk
  4. from PIL import Image, ImageTk
  5. import time
  6. import threading
  7. import tkinter as tk
  8. import dobluetooth
  9. import math
  10. import pygame
  11. # Initialize pygame and mixer specifically for MP3
  12. pygame.init()
  13. pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=2048)
  14. try:
  15. stop_sound = pygame.mixer.Sound("ding.mp3")
  16. print("Successfully loaded sound file")
  17. except Exception as e:
  18. print(f"Warning: Could not load sound file: {str(e)}")
  19. stop_sound = None
  20. # If on Windows, allow STA for Bleak
  21. if platform.system() == "Windows":
  22. try:
  23. from bleak.backends.winrt.util import allow_sta
  24. allow_sta()
  25. except ImportError:
  26. pass
  27. # UI setup
  28. ctk.set_appearance_mode("System")
  29. ctk.set_default_color_theme("blue")
  30. app = ctk.CTk()
  31. app.geometry("1440x1024")
  32. canvas = ctk.CTkCanvas(app, width=250, height=250, highlightthickness=0)
  33. canvas.config(background="white")
  34. canvas.place(x=0, y=0)
  35. # Score variables
  36. currentScore = tk.DoubleVar()
  37. currentScore.set(0)
  38. names = []
  39. scores = []
  40. nameEntry = None
  41. enterButton = None
  42. statusLabel = None
  43. dinger = None
  44. bell_image = None # Store the image reference
  45. highestScore = 0
  46. highScoreName = ""
  47. punch_on = False
  48. current_dinger_y = 900
  49. def move_dinger_step(start_y, target_y, steps_left, total_steps):
  50. global current_dinger_y
  51. if steps_left > 0:
  52. # Calculate progress (0 to 1)
  53. progress = (total_steps - steps_left) / total_steps
  54. # Calculate the current position directly between start and target
  55. new_y = start_y + (target_y - start_y) * progress
  56. # Update position
  57. current_dinger_y = new_y
  58. dinger.place_configure(y=int(new_y))
  59. # Schedule next step
  60. app.after(10, lambda: move_dinger_step(start_y, target_y, steps_left - 1, total_steps))
  61. else:
  62. # Ensure we end up exactly at target
  63. current_dinger_y = target_y
  64. dinger.place_configure(y=int(target_y))
  65. def update_dinger_position(final_score=None):
  66. global current_dinger_y
  67. start_y = current_dinger_y # Remember where we're starting from
  68. if final_score is None:
  69. target_y = 900 # Bottom position
  70. else:
  71. # Calculate target position based on score
  72. max_movement = 600
  73. score_threshold = 100
  74. # Calculate new position
  75. movement = (final_score / score_threshold) * max_movement
  76. movement = min(movement, max_movement)
  77. target_y = 900 - movement # Calculate from top position
  78. target_y = max(100, min(target_y, 900)) # Ensure within bounds
  79. # Start the animation
  80. move_dinger_step(start_y, target_y, 60, 60)
  81. def startPunch():
  82. global punch_on
  83. punch_on = True
  84. updateStatusLabel()
  85. update_dinger_position()
  86. print("STARTED PUNCH")
  87. def stopPunch():
  88. global punch_on
  89. punch_on = False
  90. updateStatusLabel()
  91. update_dinger_position(currentScore.get())
  92. if stop_sound:
  93. try:
  94. stop_sound.play()
  95. except Exception as e:
  96. print(f"Error playing sound: {str(e)}")
  97. print("STOPPED PUNCH")
  98. def resetPunch():
  99. global currentScore
  100. currentScore.set(0)
  101. update_dinger_position(0)
  102. def load_scores():
  103. global names, scores, highestScore, highScoreName
  104. try:
  105. with open("all_scores.txt", "r") as f:
  106. lines = f.readlines()
  107. for line in lines:
  108. line = line.strip()
  109. if line and "," in line: # Check if line is not empty and contains a comma
  110. try:
  111. name, score = line.split(",")
  112. score = float(score)
  113. names.append(name)
  114. scores.append(score)
  115. if score > highestScore:
  116. highestScore = score
  117. highScoreName = name
  118. except (ValueError, IndexError):
  119. # Skip malformed lines
  120. continue
  121. except FileNotFoundError:
  122. # Create the file if it doesn't exist
  123. with open("all_scores.txt", "w") as f:
  124. pass
  125. def save_new_score(name, score):
  126. with open("all_scores.txt", "a") as f:
  127. f.write(f"{name},{score}\n")
  128. def updateStatusLabel():
  129. global statusLabel
  130. if punch_on:
  131. statusLabel.configure(text="RECORDING", fg_color="green", text_color="white")
  132. else:
  133. statusLabel.configure(text="NOT RECORDING", fg_color="gray70", text_color="black")
  134. def submitScore():
  135. global highestScore, highScoreName
  136. # Get the name from the entry field
  137. entered_name = nameEntry.get().strip()
  138. if entered_name: # Only proceed if a name was entered
  139. current_score = currentScore.get()
  140. names.append(entered_name)
  141. scores.append(current_score)
  142. # Save the new score to file
  143. save_new_score(entered_name, current_score)
  144. if current_score > highestScore:
  145. highestScore = current_score
  146. highScoreName = entered_name
  147. # Clear the entry field after recording
  148. nameEntry.delete(0, 'end')
  149. # Refresh the leaderboard display
  150. placeLeaderboard()
  151. # Leaderboard & UI setup
  152. def placeLeaderboard():
  153. global nameEntry, enterButton, statusLabel, dinger, bell_image
  154. canvas.create_rectangle(0, 0, 500, 500, fill="grey20", width=2)
  155. # Get top 5 scores
  156. score_pairs = list(zip(names, scores))
  157. score_pairs.sort(key=lambda x: x[1], reverse=True)
  158. top_5_pairs = score_pairs[:5]
  159. intx = 0
  160. inty = 100
  161. for name, score in top_5_pairs:
  162. label1 = ctk.CTkLabel(width=124, height=39, text_color="white", text=name, font=("Arial", 15), master=app)
  163. label1.place(x=intx, y=inty)
  164. label2 = ctk.CTkLabel(width=124, height=39, text_color="white", text=f"{score:.2f}", font=("Arial", 15), master=app)
  165. label2.place(x=intx + 125, y=inty)
  166. inty += 40
  167. ctk.CTkLabel(width=200, height=30, text_color="white", text="Leaderboard", font=("Arial", 50), master=app).place(x=0, y=0)
  168. # Create status indicator
  169. statusLabel = ctk.CTkLabel(
  170. master=app,
  171. width=120,
  172. height=40,
  173. text="NOT RECORDING",
  174. fg_color="gray70",
  175. text_color="black",
  176. corner_radius=10,
  177. font=("Arial", 16)
  178. )
  179. statusLabel.place(x=50, y=450)
  180. ctk.CTkButton(master=app,
  181. width=120,
  182. height=80,
  183. text="START",
  184. fg_color="green",
  185. text_color="gray99",
  186. corner_radius=25,
  187. font=("Arial", 30),
  188. command=startPunch).place(x=50, y=500)
  189. ctk.CTkButton(master=app,
  190. width=120,
  191. height=80,
  192. text="STOP",
  193. fg_color="orange2",
  194. text_color="gray99",
  195. corner_radius=25,
  196. font=("Arial", 30),
  197. command=stopPunch).place(x=50, y=600)
  198. ctk.CTkButton(master=app,
  199. width=120,
  200. height=80,
  201. text="RESET",
  202. fg_color="red1",
  203. text_color="gray99",
  204. corner_radius=25,
  205. font=("Arial", 30),
  206. command=resetPunch).place(x=50, y=700)
  207. ctk.CTkLabel(width=50, height=20, text_color="white", text="Current Score", font=("Arial", 50), master=app).place(x=500, y=0)
  208. ctk.CTkLabel(width=50, height=20, text_color="white", textvariable=currentScore, font=("Arial", 50), master=app).place(x=725, y=100)
  209. nameEntry = ctk.CTkEntry(width=300, height=50, master=app, font=("Arial", 50))
  210. nameEntry.place(x=500, y=200)
  211. enterButton = ctk.CTkButton(width=50, height=75, text="ENTER", fg_color="green", font=("Arial", 20),
  212. text_color="gray99", master=app, command=submitScore)
  213. enterButton.place(x=825, y=205)
  214. # Load and display the bell image
  215. bell_image = Image.open("./BellDrawing.png")
  216. photo = ImageTk.PhotoImage(bell_image)
  217. image_label = ctk.CTkLabel(image=photo, text="", width=10, master=app)
  218. image_label.image = photo # Keep a reference to prevent garbage collection
  219. image_label.place(x=1000, y=0)
  220. # Create the dinger after the image (so it appears on top)
  221. dinger = ctk.CTkLabel(width=40, height=30, text="", fg_color="gray77", master=app)
  222. dinger.place(x=1205, y=900)
  223. # Start updating dinger position
  224. update_dinger_position()
  225. # BLE thread setup
  226. def ble_thread_func():
  227. import asyncio
  228. loop = asyncio.new_event_loop()
  229. asyncio.set_event_loop(loop)
  230. loop.run_until_complete(dobluetooth.getData())
  231. def start_ble_thread():
  232. thread = threading.Thread(target=ble_thread_func, daemon=True)
  233. thread.start()
  234. def data_loop():
  235. last_data = None
  236. last_value = 0.00
  237. while True:
  238. bledata = dobluetooth.getDataArr()
  239. if punch_on:
  240. if last_data == None:
  241. last_data = bledata
  242. current_x_accel = bledata[3]
  243. current_y_accel = bledata[4]
  244. last_x_accel = last_data[3]
  245. last_y_accel = last_data[4]
  246. value = math.sqrt((current_x_accel - last_y_accel)**2 + ((current_y_accel - last_y_accel)**2))/4
  247. print(value)
  248. if(value < 0.26):
  249. pass
  250. else:
  251. if(last_value - value > 10.00):
  252. app.after(0, lambda val=value: currentScore.set(currentScore.get() + int(val)))
  253. else:
  254. if(value - last_value > 10.00):
  255. if(value == last_value):
  256. app.after(0, lambda val=value: currentScore.set(currentScore.get() + int(val*10)))
  257. else:
  258. app.after(0, lambda val=value: currentScore.set(currentScore.get() + int(val)))
  259. else:
  260. app.after(0, lambda val=value: currentScore.set(currentScore.get() + int(val*25)))
  261. app.after(0, update_dinger_position)
  262. last_data = bledata
  263. last_value = value
  264. time.sleep(0.1)
  265. def start_data_loop_thread():
  266. thread = threading.Thread(target=data_loop, daemon=True)
  267. thread.start()
  268. # Run UI setup
  269. load_scores() # Load existing scores before setting up UI
  270. placeLeaderboard()
  271. # Start threads after UI initializes
  272. app.after(500, start_ble_thread)
  273. app.after(1000, start_data_loop_thread)
  274. app.mainloop()