pattern_manager.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import os
  2. import threading
  3. import time
  4. import random
  5. import logging
  6. from datetime import datetime
  7. from tqdm import tqdm
  8. from dune_weaver_flask.modules.serial import serial_manager
  9. from dune_weaver_flask.modules.core.state import state
  10. from math import pi
  11. # Configure logging
  12. logger = logging.getLogger(__name__)
  13. # Global state
  14. THETA_RHO_DIR = './patterns'
  15. CLEAR_PATTERNS = {
  16. "clear_from_in": "./patterns/clear_from_in.thr",
  17. "clear_from_out": "./patterns/clear_from_out.thr",
  18. "clear_sideway": "./patterns/clear_sideway.thr"
  19. }
  20. os.makedirs(THETA_RHO_DIR, exist_ok=True)
  21. current_playlist = []
  22. current_playing_index = None
  23. def list_theta_rho_files():
  24. files = []
  25. for root, _, filenames in os.walk(THETA_RHO_DIR):
  26. for file in filenames:
  27. relative_path = os.path.relpath(os.path.join(root, file), THETA_RHO_DIR)
  28. files.append(relative_path)
  29. logger.debug(f"Found {len(files)} theta-rho files")
  30. return files
  31. def parse_theta_rho_file(file_path):
  32. """Parse a theta-rho file and return a list of (theta, rho) pairs."""
  33. coordinates = []
  34. try:
  35. logger.debug(f"Parsing theta-rho file: {file_path}")
  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: {line}")
  46. continue
  47. except Exception as e:
  48. logger.error(f"Error reading file: {e}")
  49. return coordinates
  50. # Normalization Step
  51. if coordinates:
  52. first_theta = coordinates[0][0]
  53. normalized = [(theta - first_theta, rho) for theta, rho in coordinates]
  54. coordinates = normalized
  55. logger.debug(f"Parsed {len(coordinates)} coordinates from {file_path}")
  56. return coordinates
  57. def get_clear_pattern_file(clear_pattern_mode, path=None):
  58. """Return a .thr file path based on pattern_name."""
  59. if not clear_pattern_mode or clear_pattern_mode == 'none':
  60. return
  61. logger.info("Clear pattern mode: " + clear_pattern_mode)
  62. if clear_pattern_mode == "random":
  63. return random.choice(list(CLEAR_PATTERNS.values()))
  64. if clear_pattern_mode == 'adaptive':
  65. _, first_rho = parse_theta_rho_file(path)[0]
  66. if first_rho < 0.5:
  67. return CLEAR_PATTERNS['clear_from_out']
  68. else:
  69. return random.choice([CLEAR_PATTERNS['clear_from_in'], CLEAR_PATTERNS['clear_sideway']])
  70. else:
  71. return CLEAR_PATTERNS[clear_pattern_mode]
  72. def schedule_checker(schedule_hours):
  73. """Pauses/resumes execution based on a given time range."""
  74. if not schedule_hours:
  75. return
  76. start_time, end_time = schedule_hours
  77. now = datetime.now().time()
  78. if start_time <= now < end_time:
  79. if state.pause_requested:
  80. logger.info("Starting execution: Within schedule")
  81. serial_manager.update_machine_position()
  82. state.pause_requested = False
  83. with state.pause_condition:
  84. state.pause_condition.notify_all()
  85. else:
  86. if not state.pause_requested:
  87. logger.info("Pausing execution: Outside schedule")
  88. state.pause_requested = True
  89. serial_manager.update_machine_position()
  90. threading.Thread(target=wait_for_start_time, args=(schedule_hours,), daemon=True).start()
  91. def wait_for_start_time(schedule_hours):
  92. """Keep checking every 30 seconds if the time is within the schedule to resume execution."""
  93. start_time, end_time = schedule_hours
  94. while state.pause_requested:
  95. now = datetime.now().time()
  96. if start_time <= now < end_time:
  97. logger.info("Resuming execution: Within schedule")
  98. state.pause_requested = False
  99. with state.pause_condition:
  100. state.pause_condition.notify_all()
  101. break
  102. else:
  103. time.sleep(30)
  104. def move_polar(theta, rho):
  105. """
  106. This functions take in a pair of theta rho coordinate, compute the distance to travel based on current theta, rho,
  107. and translate the motion to gcode jog command and sent to grbl.
  108. Since having similar steps_per_mm will make x and y axis moves at around the same speed, we have to scale the
  109. x_steps_per_mm and y_steps_per_mm so that they are roughly the same. Here's the range of motion:
  110. X axis (angular): 50mm = 1 revolution
  111. Y axis (radial): 0 => 20mm = theta 0 (center) => 1 (perimeter)
  112. Args:
  113. theta (_type_): _description_
  114. rho (_type_): _description_
  115. """
  116. # Adding soft limit to reduce hardware sound
  117. soft_limit_inner = 0.01
  118. if rho < soft_limit_inner:
  119. rho = soft_limit_inner
  120. soft_limit_outter = 0.015
  121. if rho > (1-soft_limit_outter):
  122. rho = (1-soft_limit_outter)
  123. x_scaling_factor = 2
  124. y_scaling_factor = 5
  125. delta_theta = theta - state.current_theta
  126. delta_rho = rho - state.current_rho
  127. x_increment = delta_theta * 100 / (2 * pi * x_scaling_factor) # Scale down x from 100mm to 50mm per revolution
  128. y_increment = delta_rho * 100 / y_scaling_factor # Scale down y from 100mm to 20mm from center to perimeter
  129. x_total_steps = state.x_steps_per_mm * (100/x_scaling_factor)
  130. y_total_steps = state.y_steps_per_mm * (100/y_scaling_factor)
  131. x_increment / 50 * (x_total_steps)
  132. offset = x_increment * (x_total_steps * x_scaling_factor / (state.gear_ratio * y_total_steps * y_scaling_factor))
  133. y_increment += offset
  134. new_x_abs = state.machine_x + x_increment
  135. new_y_abs = state.machine_y + y_increment
  136. # dynamic_speed = compute_dynamic_speed(rho, max_speed=state.speed)
  137. serial_manager.send_grbl_coordinates(round(new_x_abs, 3), round(new_y_abs,3), state.speed)
  138. state.current_theta = theta
  139. state.current_rho = rho
  140. state.machine_x = new_x_abs
  141. state.machine_y = new_y_abs
  142. def reset_theta():
  143. logger.info('Resetting Theta')
  144. state.current_theta = 0
  145. serial_manager.update_machine_position()
  146. def set_speed(new_speed):
  147. state.speed = new_speed
  148. logger.info(f'Set new state.speed {new_speed}')
  149. def run_theta_rho_file(file_path, schedule_hours=None):
  150. """Run a theta-rho file by sending data in optimized batches with tqdm ETA tracking."""
  151. if not file_path:
  152. return
  153. coordinates = parse_theta_rho_file(file_path)
  154. total_coordinates = len(coordinates)
  155. if total_coordinates < 2:
  156. logger.warning("Not enough coordinates for interpolation")
  157. state.current_playing_file = None
  158. state.execution_progress = None
  159. return
  160. state.execution_progress = (0, total_coordinates, None)
  161. stop_actions()
  162. with serial_manager.serial_lock:
  163. state.current_playing_file = file_path
  164. state.execution_progress = (0, 0, None)
  165. state.stop_requested = False
  166. logger.info(f"Starting pattern execution: {file_path}")
  167. logger.info(f"t: {state.current_theta}, r: {state.current_rho}")
  168. reset_theta()
  169. with tqdm(total=total_coordinates, unit="coords", desc=f"Executing Pattern {file_path}", dynamic_ncols=True, disable=None) as pbar:
  170. for i, coordinate in enumerate(coordinates):
  171. theta, rho = coordinate
  172. if state.stop_requested:
  173. logger.info("Execution stopped by user after completing the current batch")
  174. break
  175. with state.pause_condition:
  176. while state.pause_requested:
  177. logger.info("Execution paused...")
  178. state.pause_condition.wait()
  179. schedule_checker(schedule_hours)
  180. move_polar(theta, rho)
  181. if i != 0:
  182. pbar.update(1)
  183. estimated_remaining_time = (total_coordinates - i) / pbar.format_dict['rate'] if pbar.format_dict['rate'] and total_coordinates else 0
  184. elapsed_time = pbar.format_dict['elapsed']
  185. state.execution_progress = (i, total_coordinates, estimated_remaining_time, elapsed_time)
  186. serial_manager.check_idle()
  187. state.current_playing_file = None
  188. state.execution_progress = None
  189. logger.info("Pattern execution completed")
  190. def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False, schedule_hours=None):
  191. """Run multiple .thr files in sequence with options."""
  192. global current_playing_index, current_playlist
  193. state.stop_requested = False
  194. if shuffle:
  195. random.shuffle(file_paths)
  196. logger.info("Playlist shuffled")
  197. current_playlist = file_paths
  198. while True:
  199. for idx, path in enumerate(file_paths):
  200. logger.info(f"Upcoming pattern: {path}")
  201. current_playing_index = idx
  202. schedule_checker(schedule_hours)
  203. if state.stop_requested:
  204. logger.info("Execution stopped before starting next pattern")
  205. return
  206. if clear_pattern:
  207. if state.stop_requested:
  208. logger.info("Execution stopped before running the next clear pattern")
  209. return
  210. clear_file_path = get_clear_pattern_file(clear_pattern, path)
  211. logger.info(f"Running clear pattern: {clear_file_path}")
  212. run_theta_rho_file(clear_file_path, schedule_hours)
  213. if not state.stop_requested:
  214. logger.info(f"Running pattern {idx + 1} of {len(file_paths)}: {path}")
  215. run_theta_rho_file(path, schedule_hours)
  216. if idx < len(file_paths) - 1:
  217. if state.stop_requested:
  218. logger.info("Execution stopped before running the next clear pattern")
  219. return
  220. if pause_time > 0:
  221. logger.info(f"Pausing for {pause_time} seconds")
  222. time.sleep(pause_time)
  223. if run_mode == "indefinite":
  224. logger.info("Playlist completed. Restarting as per 'indefinite' run mode")
  225. if pause_time > 0:
  226. logger.debug(f"Pausing for {pause_time} seconds before restarting")
  227. time.sleep(pause_time)
  228. if shuffle:
  229. random.shuffle(file_paths)
  230. logger.info("Playlist reshuffled for the next loop")
  231. continue
  232. else:
  233. logger.info("Playlist completed")
  234. break
  235. logger.info("All requested patterns completed (or stopped)")
  236. def stop_actions():
  237. """Stop all current pattern execution."""
  238. with state.pause_condition:
  239. state.pause_requested = False
  240. state.stop_requested = True
  241. current_playing_index = None
  242. current_playlist = None
  243. state.is_clearing = False
  244. state.current_playing_file = None
  245. state.execution_progress = None
  246. serial_manager.update_machine_position()
  247. def get_status():
  248. """Get the current execution status."""
  249. # Update state.is_clearing based on current file
  250. if state.current_playing_file in CLEAR_PATTERNS.values():
  251. state.is_clearing = True
  252. else:
  253. state.is_clearing = False
  254. return {
  255. "ser_port": serial_manager.get_port(),
  256. "stop_requested": state.stop_requested,
  257. "pause_requested": state.pause_requested,
  258. "current_playing_file": state.current_playing_file,
  259. "execution_progress": state.execution_progress,
  260. "current_playing_index": current_playing_index,
  261. "current_playlist": current_playlist,
  262. "is_clearing": state.is_clearing
  263. }