GUI.py 10 KB

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