pattern_manager.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import os
  2. import json
  3. import random
  4. import threading
  5. from datetime import datetime
  6. import time
  7. from ..serial.serial_manager import send_coordinate_batch, reset_theta, send_command
  8. import logging
  9. logger = logging.getLogger(__name__)
  10. # Configuration
  11. THETA_RHO_DIR = './patterns'
  12. CLEAR_PATTERNS = {
  13. "clear_from_in": "./patterns/clear_from_in.thr",
  14. "clear_from_out": "./patterns/clear_from_out.thr",
  15. "clear_sideway": "./patterns/clear_sideway.thr"
  16. }
  17. os.makedirs(THETA_RHO_DIR, exist_ok=True)
  18. # Global variables for execution state
  19. stop_requested = False
  20. pause_requested = False
  21. pause_condition = threading.Condition()
  22. current_playing_file = None
  23. execution_progress = None
  24. current_playing_index = None
  25. current_playlist = None
  26. is_clearing = False
  27. PLAYLISTS_FILE = os.path.join(os.getcwd(), "playlists.json")
  28. # Ensure the playlists file exists
  29. if not os.path.exists(PLAYLISTS_FILE):
  30. with open(PLAYLISTS_FILE, "w") as f:
  31. json.dump({}, f, indent=2)
  32. def parse_theta_rho_file(file_path):
  33. """Parse a theta-rho file and return a list of (theta, rho) pairs."""
  34. coordinates = []
  35. try:
  36. with open(file_path, 'r') as file:
  37. for line in file:
  38. line = line.strip()
  39. if not line or line.startswith("#"):
  40. continue
  41. try:
  42. theta, rho = map(float, line.split())
  43. coordinates.append((theta, rho))
  44. except ValueError:
  45. logger.warning(f"Skipping invalid line in {file_path}: {line}")
  46. continue
  47. except FileNotFoundError:
  48. logger.error(f"Theta-rho file not found: {file_path}")
  49. return coordinates
  50. except Exception as e:
  51. logger.error(f"Error reading theta-rho file {file_path}: {str(e)}", exc_info=True)
  52. return coordinates
  53. # Normalize coordinates
  54. if coordinates:
  55. first_theta = coordinates[0][0]
  56. normalized = [(theta - first_theta, rho) for theta, rho in coordinates]
  57. coordinates = normalized
  58. return coordinates
  59. def get_clear_pattern_file(pattern_name):
  60. """Return a .thr file path based on pattern_name."""
  61. if pattern_name == "random":
  62. return random.choice(list(CLEAR_PATTERNS.values()))
  63. return CLEAR_PATTERNS.get(pattern_name, CLEAR_PATTERNS["clear_from_in"])
  64. def schedule_checker(schedule_hours):
  65. """Check if execution should be paused/resumed based on schedule."""
  66. global pause_requested
  67. if not schedule_hours:
  68. return
  69. start_time, end_time = schedule_hours
  70. now = datetime.now().time()
  71. if start_time <= now < end_time:
  72. if pause_requested:
  73. print("Starting execution: Within schedule.")
  74. pause_requested = False
  75. with pause_condition:
  76. pause_condition.notify_all()
  77. else:
  78. if not pause_requested:
  79. print("Pausing execution: Outside schedule.")
  80. pause_requested = True
  81. threading.Thread(target=wait_for_start_time, args=(schedule_hours,), daemon=True).start()
  82. def wait_for_start_time(schedule_hours):
  83. """Keep checking if it's time to resume execution."""
  84. global pause_requested
  85. start_time, end_time = schedule_hours
  86. while pause_requested:
  87. now = datetime.now().time()
  88. if start_time <= now < end_time:
  89. print("Resuming execution: Within schedule.")
  90. pause_requested = False
  91. with pause_condition:
  92. pause_condition.notify_all()
  93. break
  94. else:
  95. time.sleep(30)
  96. def run_theta_rho_file(file_path, schedule_hours=None):
  97. """Run a single theta-rho file."""
  98. global stop_requested, current_playing_file, execution_progress
  99. stop_requested = False
  100. current_playing_file = file_path
  101. execution_progress = (0, 0)
  102. coordinates = parse_theta_rho_file(file_path)
  103. total_coordinates = len(coordinates)
  104. if total_coordinates < 2:
  105. logger.error(f"Not enough coordinates for interpolation in file: {file_path}")
  106. current_playing_file = None
  107. execution_progress = None
  108. return
  109. try:
  110. execution_progress = (0, total_coordinates)
  111. batch_size = 10
  112. for i in range(0, total_coordinates, batch_size):
  113. if stop_requested:
  114. logger.info("Execution stopped by user after completing the current batch.")
  115. break
  116. with pause_condition:
  117. while pause_requested:
  118. logger.info("Execution paused...")
  119. pause_condition.wait()
  120. batch = coordinates[i:i + batch_size]
  121. if i == 0:
  122. send_coordinate_batch(batch)
  123. execution_progress = (i + batch_size, total_coordinates)
  124. continue
  125. while True:
  126. schedule_checker(schedule_hours)
  127. response = send_command("R")
  128. if response == "R":
  129. send_coordinate_batch(batch)
  130. execution_progress = (i + batch_size, total_coordinates)
  131. break
  132. reset_theta()
  133. send_command("FINISHED")
  134. except Exception as e:
  135. logger.error(f"Error executing theta-rho file {file_path}: {str(e)}", exc_info=True)
  136. finally:
  137. current_playing_file = None
  138. execution_progress = None
  139. logger.info("Pattern execution completed.")
  140. def run_theta_rho_files(
  141. file_paths,
  142. pause_time=0,
  143. clear_pattern=None,
  144. run_mode="single",
  145. shuffle=False,
  146. schedule_hours=None
  147. ):
  148. """Run multiple theta-rho files with various options."""
  149. global stop_requested, current_playlist, current_playing_index, is_clearing
  150. stop_requested = False
  151. if shuffle:
  152. random.shuffle(file_paths)
  153. print("Playlist shuffled.")
  154. current_playlist = file_paths
  155. while True:
  156. for idx, path in enumerate(file_paths):
  157. current_playing_index = idx
  158. schedule_checker(schedule_hours)
  159. if stop_requested:
  160. print("Execution stopped before starting next pattern.")
  161. return
  162. if clear_pattern:
  163. if stop_requested:
  164. print("Execution stopped before running the next clear pattern.")
  165. return
  166. clear_file_path = get_clear_pattern_file(clear_pattern)
  167. print(f"Running clear pattern: {clear_file_path}")
  168. is_clearing = True
  169. run_theta_rho_file(clear_file_path, schedule_hours)
  170. is_clearing = False
  171. if not stop_requested:
  172. print(f"Running pattern {idx + 1} of {len(file_paths)}: {path}")
  173. run_theta_rho_file(path, schedule_hours)
  174. if idx < len(file_paths) - 1:
  175. if stop_requested:
  176. print("Execution stopped before running the next clear pattern.")
  177. return
  178. if pause_time > 0:
  179. print(f"Pausing for {pause_time} seconds...")
  180. time.sleep(pause_time)
  181. if run_mode == "indefinite":
  182. print("Playlist completed. Restarting as per 'indefinite' run mode.")
  183. if pause_time > 0:
  184. print(f"Pausing for {pause_time} seconds before restarting...")
  185. time.sleep(pause_time)
  186. if shuffle:
  187. random.shuffle(file_paths)
  188. print("Playlist reshuffled for the next loop.")
  189. continue
  190. else:
  191. print("Playlist completed.")
  192. break
  193. reset_theta()
  194. send_command("FINISHED")
  195. print("All requested patterns completed (or stopped).")
  196. def get_execution_status():
  197. """Get the current execution status."""
  198. return {
  199. "stop_requested": stop_requested,
  200. "pause_requested": pause_requested,
  201. "current_playing_file": current_playing_file,
  202. "execution_progress": execution_progress,
  203. "current_playing_index": current_playing_index,
  204. "current_playlist": current_playlist,
  205. "is_clearing": is_clearing
  206. }
  207. def stop_execution():
  208. """Stop the current execution."""
  209. global stop_requested, pause_requested, current_playing_index
  210. global current_playlist, is_clearing, current_playing_file, execution_progress
  211. with pause_condition:
  212. pause_requested = False
  213. pause_condition.notify_all()
  214. stop_requested = True
  215. current_playing_index = None
  216. current_playlist = None
  217. is_clearing = False
  218. current_playing_file = None
  219. execution_progress = None
  220. def pause_execution():
  221. """Pause the current execution."""
  222. global pause_requested
  223. with pause_condition:
  224. pause_requested = True
  225. def resume_execution():
  226. """Resume the current execution."""
  227. global pause_requested
  228. with pause_condition:
  229. pause_requested = False
  230. pause_condition.notify_all()