public_stable.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import customtkinter as ctk
  2. import time
  3. import math
  4. import random
  5. import csv
  6. import tkinter as tk
  7. import customtkinter as ctk
  8. import time
  9. ## cc maximilian scheinast-peter
  10. ## last update 11.04.2024
  11. ## dieses programm funktioniert ähnlich wie ein digitales Oszi zur Darstellung von Vitalkurven
  12. ## Attribution-NonCommercial license
  13. def get_data(): #example heartfunction
  14. timestamp = time.time()## dont change
  15. ## logic for get one datapoint like a read_value function
  16. t = (timestamp % 1) * 2 * math.pi
  17. p_wave = 5 * math.sin(t)
  18. qrs_complex = 40 * math.sin(1.5 * t) * math.exp(-0.25 * t**2)
  19. t_wave = 10 * math.sin(2 * t) * math.exp(-0.5 * t**2)
  20. value = 50 + p_wave + qrs_complex + t_wave
  21. value = max(0, min(value, 100))
  22. wait_for_next_millisecond()
  23. return timestamp, value
  24. def wait_for_next_millisecond():
  25. """Waits until the next full millisecond."""
  26. current_time = time.time()
  27. next_millisecond = (int(current_time * 1000) + 1) / 1000
  28. time.sleep(next_millisecond - current_time)
  29. # Function to generate realistic ECG data
  30. class EKGApp:
  31. def __init__(self, master):
  32. self.master = master
  33. master.title("EKG Visualization")
  34. # Main Frame
  35. main_frame = ctk.CTkFrame(master)
  36. main_frame.pack(fill=ctk.BOTH, expand=True)
  37. # Single Sweep Canvas
  38. self.single_canvas = ctk.CTkCanvas(main_frame, width=600, height=300)
  39. self.single_canvas.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True)
  40. # Multi Sweep Canvas
  41. self.multi_canvas = ctk.CTkCanvas(main_frame, width=600, height=300)
  42. self.multi_canvas.pack(side=ctk.LEFT, fill=ctk.BOTH, expand=True)
  43. # Input Frame
  44. input_frame = ctk.CTkFrame(main_frame)
  45. input_frame.pack(side=ctk.RIGHT, fill=ctk.Y)
  46. # Data Structures
  47. self.single_data = []
  48. self.multi_data = []
  49. self.logged_data = []
  50. # Time Variables
  51. self.start_time = time.time()
  52. self.multi_sweep_duration = 30 # Initial Laufbanddauer
  53. # Trigger Variables
  54. self.trigger_level = 50
  55. self.trigger_armed = False
  56. self.trigger_paused = False
  57. self.trigger_timestamp = None
  58. self.last_trigger_timestamp = None
  59. self.last_value = 0
  60. self.trigger_count = 0
  61. # Cooldown Variables
  62. self.cooldown_time = 0
  63. self.cooldown_active = False
  64. # Logging Variables
  65. self.logging_active = False
  66. # Input Fields and Buttons (with consistent size)
  67. self.create_input_fields(input_frame)
  68. # Logging Frame
  69. logging_frame = ctk.CTkFrame(master)
  70. logging_frame.pack(fill=ctk.X)
  71. self.create_logging_buttons(logging_frame)
  72. # Update Data Periodically
  73. self.update_data()
  74. # Resize Handling
  75. self.single_canvas.bind("<Configure>", self.on_resize)
  76. self.multi_canvas.bind("<Configure>", self.on_resize)
  77. def create_input_fields(self, frame):
  78. # Consistent width for all input elements
  79. input_width = 150
  80. # Trigger Level
  81. ctk.CTkLabel(frame, text="Trigger Level:").pack()
  82. self.trigger_entry = ctk.CTkEntry(frame, width=input_width)
  83. self.trigger_entry.insert(0, str(self.trigger_level))
  84. self.trigger_entry.pack()
  85. # Flank Selection
  86. ctk.CTkLabel(frame, text="Flanke:").pack()
  87. self.flank_var = ctk.StringVar(value="Steigende Flanke")
  88. self.flank_combobox = ctk.CTkComboBox(frame, variable=self.flank_var, values=["Steigende Flanke", "Fallende Flanke"], width=input_width)
  89. self.flank_combobox.pack()
  90. # Cooldown
  91. ctk.CTkLabel(frame, text="Cooldown (Sekunden):").pack()
  92. self.cooldown_entry = ctk.CTkEntry(frame, width=input_width)
  93. self.cooldown_entry.insert(0, "0")
  94. self.cooldown_entry.pack()
  95. self.cooldown_button = ctk.CTkButton(frame, text="Cooldown setzen", command=self.set_cooldown, width=input_width)
  96. self.cooldown_button.pack()
  97. # Ax Time (X-Achsen-Zeit)
  98. ctk.CTkLabel(frame, text="Ax-Zeit (Sekunden):").pack()
  99. self.ax_time_entry = ctk.CTkEntry(frame, width=input_width)
  100. self.ax_time_entry.insert(0, str(self.multi_sweep_duration)) # Initial value
  101. self.ax_time_entry.pack()
  102. self.ax_time_button = ctk.CTkButton(frame, text="Ax-Zeit setzen", command=self.set_ax_time, width=input_width)
  103. self.ax_time_button.pack()
  104. # Trigger Buttons
  105. self.trigger_button = ctk.CTkButton(frame, text="Trigger starten", command=self.toggle_trigger, width=input_width)
  106. self.trigger_button.pack()
  107. self.pause_button = ctk.CTkButton(frame, text="Trigger pausieren", command=self.pause_trigger, state=ctk.DISABLED, width=input_width)
  108. self.pause_button.pack()
  109. # Multi Sweep Duration (Laufbanddauer)
  110. ctk.CTkLabel(frame, text="Laufbanddauer (Sekunden):").pack()
  111. self.duration_entry = ctk.CTkEntry(frame, width=input_width)
  112. self.duration_entry.insert(0, "30")
  113. self.duration_entry.pack()
  114. self.duration_button = ctk.CTkButton(frame, text="Dauer setzen", command=self.set_duration, width=input_width)
  115. self.duration_button.pack()
  116. def set_ax_time(self):
  117. try:
  118. self.multi_sweep_duration = float(self.ax_time_entry.get())
  119. except ValueError:
  120. pass
  121. def create_logging_buttons(self, frame):
  122. # Consistent width for logging buttons
  123. button_width = 120
  124. self.start_logging_button = ctk.CTkButton(frame, text="Start Logging", command=self.start_logging, width=button_width)
  125. self.start_logging_button.pack(side=ctk.LEFT)
  126. self.end_logging_button = ctk.CTkButton(frame, text="End Logging", command=self.end_logging, state=ctk.DISABLED, width=button_width)
  127. self.end_logging_button.pack(side=ctk.LEFT)
  128. self.save_data_button = ctk.CTkButton(frame, text="Save Data", command=self.save_data, state=ctk.DISABLED, width=button_width)
  129. self.save_data_button.pack(side=ctk.LEFT)
  130. def set_cooldown(self):
  131. try:
  132. self.cooldown_time = float(self.cooldown_entry.get())
  133. except ValueError:
  134. pass
  135. def set_duration(self):
  136. try:
  137. self.multi_sweep_duration = float(self.duration_entry.get())
  138. except ValueError:
  139. pass
  140. def toggle_trigger(self):
  141. if self.trigger_armed:
  142. self.trigger_armed = False
  143. self.trigger_button.configure(text="Trigger starten")
  144. self.pause_button.configure(state=ctk.DISABLED)
  145. self.single_data = []
  146. self.draw_single_canvas()
  147. else:
  148. try:
  149. self.trigger_level = float(self.trigger_entry.get())
  150. except ValueError:
  151. pass
  152. self.trigger_armed = True
  153. self.trigger_button.configure(text="Trigger stoppen")
  154. self.pause_button.configure(state=ctk.NORMAL)
  155. self.trigger_count = 0
  156. def pause_trigger(self):
  157. self.trigger_paused = not self.trigger_paused
  158. if self.trigger_paused:
  159. self.pause_button.configure(text="Trigger fortsetzen")
  160. else:
  161. self.pause_button.configure(text="Trigger pausieren")
  162. def start_logging(self):
  163. self.logging_active = True
  164. self.logged_data = []
  165. self.start_logging_button.configure(state=ctk.DISABLED)
  166. self.end_logging_button.configure(state=ctk.NORMAL)
  167. def end_logging(self):
  168. self.logging_active = False
  169. self.start_logging_button.configure(state=ctk.NORMAL)
  170. self.end_logging_button.configure(state=ctk.DISABLED)
  171. self.save_data_button.configure(state=ctk.NORMAL)
  172. def save_data(self):
  173. # Popup for file name
  174. print(123)
  175. file_name = ctk.CTkInputDialog(text="Enter file name:", title="Save Data").get_input()
  176. if file_name:
  177. try:
  178. with open(f"{file_name}.csv", "w", newline="", encoding='utf-8') as csvfile:
  179. writer = csv.writer(csvfile)
  180. writer.writerow(["Timestamp", "Value"])
  181. writer.writerows(self.logged_data)
  182. self.save_data_button.configure(state=ctk.DISABLED)
  183. except Exception as e:
  184. print(f"Error saving data: {e}")
  185. def update_data(self):
  186. # Get EKG Data
  187. timestamp, value = get_data()
  188. # Update Multi Sweep Data
  189. self.multi_data.append((timestamp, value))
  190. if timestamp - self.multi_data[0][0] > self.multi_sweep_duration:
  191. self.multi_data.pop(0)
  192. # Trigger Logic
  193. if self.trigger_armed and not self.trigger_paused and not self.cooldown_active:
  194. condition = (value >= self.trigger_level and self.last_value < self.trigger_level) if self.flank_var.get() == "Steigende Flanke" else (value <= self.trigger_level and self.last_value > self.trigger_level)
  195. if condition:
  196. self.last_trigger_timestamp = timestamp
  197. self.single_data = []
  198. self.trigger_count = 0
  199. if self.cooldown_time > 0:
  200. self.cooldown_active = True
  201. self.master.after(int(self.cooldown_time * 1000), self.end_cooldown)
  202. # Update Single Sweep Data
  203. if self.last_trigger_timestamp is not None and self.trigger_armed and not self.trigger_paused:
  204. self.single_data.append((timestamp, value))
  205. # Update Logged Data
  206. if self.logging_active:
  207. self.logged_data.append((timestamp, value))
  208. # Draw Canvases
  209. self.draw_single_canvas()
  210. self.draw_multi_canvas()
  211. # Store last value and update trigger count
  212. self.last_value = value
  213. if self.trigger_armed and not self.trigger_paused:
  214. self.trigger_count += 1
  215. # Call again after 1ms
  216. self.master.after(1, self.update_data)
  217. def end_cooldown(self):
  218. self.cooldown_active = False
  219. def draw_single_canvas(self):
  220. self.single_canvas.delete("all")
  221. if not self.single_data or self.last_trigger_timestamp is None:
  222. return
  223. last_x, last_y = None, None
  224. for timestamp, value in self.single_data:
  225. x = (timestamp - self.last_trigger_timestamp) * self.single_canvas.winfo_width() / self.multi_sweep_duration
  226. y = self.single_canvas.winfo_height() - (value * self.single_canvas.winfo_height() / 100)
  227. if last_x is not None:
  228. self.single_canvas.create_line(last_x, last_y, x, y, fill="blue")
  229. last_x, last_y = x, y
  230. def draw_multi_canvas(self):
  231. self.multi_canvas.delete("all")
  232. if not self.multi_data:
  233. return
  234. duration = max(self.multi_data[-1][0] - self.multi_data[0][0], 0.001)
  235. x_scale = self.multi_canvas.winfo_width() / duration
  236. y_scale = self.multi_canvas.winfo_height() / 100
  237. last_x, last_y = None, None
  238. for timestamp, value in self.multi_data:
  239. x = (timestamp - self.multi_data[0][0]) * x_scale
  240. y = self.multi_canvas.winfo_height() - (value * y_scale)
  241. if last_x is not None:
  242. self.multi_canvas.create_line(last_x, last_y, x, y, fill="blue")
  243. last_x, last_y = x, y
  244. self.draw_time_axis(self.multi_canvas, self.multi_data[0][0], duration)
  245. trigger_y = self.multi_canvas.winfo_height() - (self.trigger_level * y_scale)
  246. self.multi_canvas.create_line(self.multi_canvas.winfo_width() - 10, trigger_y, self.multi_canvas.winfo_width(), trigger_y + 5, fill="red")
  247. self.multi_canvas.create_line(self.multi_canvas.winfo_width() - 10, trigger_y, self.multi_canvas.winfo_width(), trigger_y - 5, fill="red")
  248. def draw_time_axis(self, canvas, start_time, duration):
  249. for i in range(int(duration) + 1):
  250. x = i * canvas.winfo_width() / duration
  251. canvas.create_line(x, 0, x, canvas.winfo_height(), fill="gray", dash=(2, 2))
  252. canvas.create_text(x, canvas.winfo_height() - 10, text=f"{i:.1f}s", anchor=ctk.N)
  253. def on_resize(self, event):
  254. self.draw_single_canvas()
  255. self.draw_multi_canvas()
  256. if __name__ == "__main__":
  257. ctk.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", "Light"
  258. ctk.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
  259. root = ctk.CTk()
  260. app = EKGApp(root)
  261. root.mainloop()