GUI.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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. def stopPunch():
  87. global punch_on
  88. punch_on = False
  89. updateStatusLabel()
  90. update_dinger_position(currentScore.get())
  91. if stop_sound:
  92. try:
  93. stop_sound.play()
  94. except Exception as e:
  95. print(f"Error playing sound: {str(e)}")
  96. def resetPunch():
  97. global currentScore
  98. currentScore.set(0)
  99. update_dinger_position(0)
  100. def load_scores():
  101. global names, scores, highestScore, highScoreName
  102. try:
  103. with open("all_scores.txt", "r") as f:
  104. lines = f.readlines()
  105. for line in lines:
  106. name, score = line.strip().split(",")
  107. score = float(score)
  108. names.append(name)
  109. scores.append(score)
  110. if score > highestScore:
  111. highestScore = score
  112. highScoreName = name
  113. except FileNotFoundError:
  114. # Create the file if it doesn't exist
  115. with open("all_scores.txt", "w") as f:
  116. pass
  117. def save_new_score(name, score):
  118. with open("all_scores.txt", "a") as f:
  119. f.write(f"{name},{score}\n")
  120. def updateStatusLabel():
  121. global statusLabel
  122. if punch_on:
  123. statusLabel.configure(text="RECORDING", fg_color="green", text_color="white")
  124. else:
  125. statusLabel.configure(text="NOT RECORDING", fg_color="gray70", text_color="black")
  126. def submitScore():
  127. global highestScore, highScoreName
  128. # Get the name from the entry field
  129. entered_name = nameEntry.get().strip()
  130. if entered_name: # Only proceed if a name was entered
  131. current_score = currentScore.get()
  132. names.append(entered_name)
  133. scores.append(current_score)
  134. # Save the new score to file
  135. save_new_score(entered_name, current_score)
  136. if current_score > highestScore:
  137. highestScore = current_score
  138. highScoreName = entered_name
  139. # Clear the entry field after recording
  140. nameEntry.delete(0, 'end')
  141. # Refresh the leaderboard display
  142. placeLeaderboard()
  143. # Leaderboard & UI setup
  144. def placeLeaderboard():
  145. global nameEntry, enterButton, statusLabel, dinger, bell_image
  146. canvas.create_rectangle(0, 0, 500, 500, fill="grey20", width=2)
  147. # Get top 5 scores
  148. score_pairs = list(zip(names, scores))
  149. score_pairs.sort(key=lambda x: x[1], reverse=True)
  150. top_5_pairs = score_pairs[:5]
  151. intx = 0
  152. inty = 60
  153. for name, score in top_5_pairs:
  154. label1 = ctk.CTkLabel(width=124, height=39, text_color="white", text=name, font=("Arial", 15), master=app)
  155. label1.place(x=intx, y=inty)
  156. label2 = ctk.CTkLabel(width=124, height=39, text_color="white", text=f"{score:.2f}", font=("Arial", 15), master=app)
  157. label2.place(x=intx + 125, y=inty)
  158. inty += 40
  159. ctk.CTkLabel(width=249, height=39, text_color="white", text="Leaderboard", font=("Arial", 50), master=app).place(x=0, y=0)
  160. # Create status indicator
  161. statusLabel = ctk.CTkLabel(
  162. master=app,
  163. width=120,
  164. height=40,
  165. text="NOT RECORDING",
  166. fg_color="gray70",
  167. text_color="black",
  168. corner_radius=10,
  169. font=("Arial", 16)
  170. )
  171. statusLabel.place(x=50, y=450)
  172. ctk.CTkButton(master=app,
  173. width=120,
  174. height=80,
  175. text="START",
  176. fg_color="green",
  177. text_color="gray99",
  178. corner_radius=25,
  179. font=("Arial", 30),
  180. command=startPunch).place(x=50, y=500)
  181. ctk.CTkButton(master=app,
  182. width=120,
  183. height=80,
  184. text="STOP",
  185. fg_color="orange2",
  186. text_color="gray99",
  187. corner_radius=25,
  188. font=("Arial", 30),
  189. command=stopPunch).place(x=50, y=600)
  190. ctk.CTkButton(master=app,
  191. width=120,
  192. height=80,
  193. text="RESET",
  194. fg_color="red1",
  195. text_color="gray99",
  196. corner_radius=25,
  197. font=("Arial", 30),
  198. command=resetPunch).place(x=50, y=700)
  199. ctk.CTkLabel(width=50, height=20, text_color="white", text="Current Score", font=("Arial", 50), master=app).place(x=350, y=20)
  200. ctk.CTkLabel(width=50, height=20, text_color="white", textvariable=currentScore, font=("Arial", 50), master=app).place(x=500, y=100)
  201. nameEntry = ctk.CTkEntry(width=300, height=50, master=app, font=("Arial", 50))
  202. nameEntry.place(x=370, y=200)
  203. enterButton = ctk.CTkButton(width=50, height=50, text="ENTER", fg_color="green", font=("Arial", 20),
  204. text_color="gray99", master=app, command=submitScore)
  205. enterButton.place(x=670, y=205)
  206. # Load and display the bell image
  207. bell_image = Image.open("./BellDrawing.png")
  208. photo = ImageTk.PhotoImage(bell_image)
  209. image_label = ctk.CTkLabel(image=photo, text="", width=10, master=app)
  210. image_label.image = photo # Keep a reference to prevent garbage collection
  211. image_label.place(x=800, y=0)
  212. # Create the dinger after the image (so it appears on top)
  213. dinger = ctk.CTkLabel(width=40, height=30, text="", fg_color="gray77", master=app)
  214. dinger.place(x=1005, y=900)
  215. # Start updating dinger position
  216. update_dinger_position()
  217. # BLE thread setup
  218. def ble_thread_func():
  219. import asyncio
  220. loop = asyncio.new_event_loop()
  221. asyncio.set_event_loop(loop)
  222. loop.run_until_complete(dobluetooth.getData())
  223. def start_ble_thread():
  224. thread = threading.Thread(target=ble_thread_func, daemon=True)
  225. thread.start()
  226. def data_loop():
  227. last_data = None
  228. last_value = 0.00
  229. while True:
  230. bledata = dobluetooth.getDataArr()
  231. if punch_on:
  232. if last_data == None:
  233. last_data = bledata
  234. current_x_accel = bledata[3]
  235. current_y_accel = bledata[4]
  236. last_x_accel = last_data[3]
  237. last_y_accel = last_data[4]
  238. value = math.sqrt((current_x_accel - last_y_accel)**2 + ((current_y_accel - last_y_accel)**2))/4
  239. print(value)
  240. if(value < 0.26):
  241. pass
  242. else:
  243. if(last_value - value > 10.00):
  244. app.after(0, lambda val=value: currentScore.set(currentScore.get() + int(val)))
  245. else:
  246. if(value - last_value > 10.00):
  247. if(value == last_value):
  248. app.after(0, lambda val=value: currentScore.set(currentScore.get() + int(val*10)))
  249. else:
  250. app.after(0, lambda val=value: currentScore.set(currentScore.get() + int(val)))
  251. else:
  252. app.after(0, lambda val=value: currentScore.set(currentScore.get() + int(val*25)))
  253. app.after(0, update_dinger_position)
  254. last_data = bledata
  255. last_value = value
  256. time.sleep(0.1)
  257. def start_data_loop_thread():
  258. thread = threading.Thread(target=data_loop, daemon=True)
  259. thread.start()
  260. # Run UI setup
  261. load_scores() # Load existing scores before setting up UI
  262. placeLeaderboard()
  263. # Start threads after UI initializes
  264. app.after(500, start_ble_thread)
  265. app.after(1000, start_data_loop_thread)
  266. app.mainloop()