app.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. from flask import Flask, request, jsonify, render_template
  2. import os
  3. import serial
  4. import time
  5. import random
  6. import threading
  7. import serial.tools.list_ports
  8. import math
  9. import json
  10. app = Flask(__name__)
  11. # Configuration
  12. THETA_RHO_DIR = './patterns'
  13. IGNORE_PORTS = ['/dev/cu.debug-console', '/dev/cu.Bluetooth-Incoming-Port']
  14. CLEAR_PATTERNS = {
  15. "clear_from_in": "./patterns/clear_from_in.thr",
  16. "clear_from_out": "./patterns/clear_from_out.thr",
  17. "clear_sideway": "./patterns/clear_sideway.thr"
  18. }
  19. os.makedirs(THETA_RHO_DIR, exist_ok=True)
  20. # Serial connection (First available will be selected by default)
  21. ser = None
  22. ser_port = None # Global variable to store the serial port name
  23. stop_requested = False
  24. serial_lock = threading.Lock()
  25. PLAYLISTS_FILE = os.path.join(os.getcwd(), "playlists.json")
  26. # Ensure the file exists and contains at least an empty JSON object
  27. if not os.path.exists(PLAYLISTS_FILE):
  28. with open(PLAYLISTS_FILE, "w") as f:
  29. json.dump({}, f, indent=2)
  30. def list_serial_ports():
  31. """Return a list of available serial ports."""
  32. ports = serial.tools.list_ports.comports()
  33. return [port.device for port in ports if port.device not in IGNORE_PORTS]
  34. def connect_to_serial(port=None, baudrate=115200):
  35. """Automatically connect to the first available serial port or a specified port."""
  36. global ser, ser_port
  37. try:
  38. if port is None:
  39. ports = list_serial_ports()
  40. if not ports:
  41. print("No serial port connected")
  42. return False
  43. port = ports[0] # Auto-select the first available port
  44. with serial_lock:
  45. if ser and ser.is_open:
  46. ser.close()
  47. ser = serial.Serial(port, baudrate)
  48. ser_port = port # Store the connected port globally
  49. print(f"Connected to serial port: {port}")
  50. time.sleep(2) # Allow time for the connection to establish
  51. return True # Successfully connected
  52. except serial.SerialException as e:
  53. print(f"Failed to connect to serial port {port}: {e}")
  54. port = None # Reset the port to try the next available one
  55. print("Max retries reached. Could not connect to a serial port.")
  56. return False
  57. def disconnect_serial():
  58. """Disconnect the current serial connection."""
  59. global ser, ser_port
  60. if ser and ser.is_open:
  61. ser.close()
  62. ser = None
  63. ser_port = None # Reset the port name
  64. def restart_serial(port, baudrate=115200):
  65. """Restart the serial connection."""
  66. disconnect_serial()
  67. connect_to_serial(port, baudrate)
  68. def parse_theta_rho_file(file_path):
  69. """
  70. Parse a theta-rho file and return a list of (theta, rho) pairs.
  71. Normalizes the list so the first theta is always 0.
  72. """
  73. coordinates = []
  74. try:
  75. with open(file_path, 'r') as file:
  76. for line in file:
  77. line = line.strip()
  78. # Skip header or comment lines (starting with '#' or empty lines)
  79. if not line or line.startswith("#"):
  80. continue
  81. # Parse lines with theta and rho separated by spaces
  82. try:
  83. theta, rho = map(float, line.split())
  84. coordinates.append((theta, rho))
  85. except ValueError:
  86. print(f"Skipping invalid line: {line}")
  87. continue
  88. except Exception as e:
  89. print(f"Error reading file: {e}")
  90. return coordinates
  91. # ---- Normalization Step ----
  92. if coordinates:
  93. # Take the first coordinate's theta
  94. first_theta = coordinates[0][0]
  95. # Shift all thetas so the first coordinate has theta=0
  96. normalized = []
  97. for (theta, rho) in coordinates:
  98. if rho > 1:
  99. rho = 1
  100. elif rho < 0:
  101. rho = 0
  102. normalized.append((theta - first_theta, rho))
  103. # Replace original list with normalized data
  104. coordinates = normalized
  105. return coordinates
  106. def send_coordinate_batch(ser, coordinates):
  107. """Send a batch of theta-rho pairs to the Arduino."""
  108. # print("Sending batch:", coordinates)
  109. batch_str = ";".join(f"{theta:.5f},{rho:.5f}" for theta, rho in coordinates) + ";\n"
  110. ser.write(batch_str.encode())
  111. def send_command(command):
  112. """Send a single command to the Arduino."""
  113. ser.write(f"{command}\n".encode())
  114. print(f"Sent: {command}")
  115. # Wait for "DONE" acknowledgment from Arduino
  116. while True:
  117. with serial_lock:
  118. if ser.in_waiting > 0:
  119. response = ser.readline().decode().strip()
  120. print(f"Arduino response: {response}")
  121. if response == "DONE":
  122. print("Command execution completed.")
  123. break
  124. def run_theta_rho_file(file_path):
  125. """Run a theta-rho file by sending data in optimized batches."""
  126. global stop_requested
  127. stop_requested = False
  128. coordinates = parse_theta_rho_file(file_path)
  129. if len(coordinates) < 2:
  130. print("Not enough coordinates for interpolation.")
  131. return
  132. # Optimize batch size for smoother execution
  133. batch_size = 10 # Smaller batches may smooth movement further
  134. for i in range(0, len(coordinates), batch_size):
  135. # Check stop_requested flag after sending the batch
  136. if stop_requested:
  137. print("Execution stopped by user after completing the current batch.")
  138. break
  139. batch = coordinates[i:i + batch_size]
  140. if i == 0:
  141. send_coordinate_batch(ser, batch)
  142. continue
  143. # Wait until Arduino is READY before sending the batch
  144. while True:
  145. with serial_lock:
  146. if ser.in_waiting > 0:
  147. response = ser.readline().decode().strip()
  148. if response == "R":
  149. send_coordinate_batch(ser, batch)
  150. break
  151. else:
  152. print(f"Arduino response: {response}")
  153. # Reset theta after execution or stopping
  154. reset_theta()
  155. ser.write("FINISHED\n".encode())
  156. def get_clear_pattern_file(pattern_name):
  157. """Return a .thr file path based on pattern_name."""
  158. if pattern_name == "random":
  159. # Randomly pick one of the three known patterns
  160. return random.choice(list(CLEAR_PATTERNS.values()))
  161. # If pattern_name is invalid or absent, default to 'clear_from_in'
  162. return CLEAR_PATTERNS.get(pattern_name, CLEAR_PATTERNS["clear_from_in"])
  163. def run_theta_rho_files(
  164. file_paths,
  165. pause_time=0,
  166. clear_pattern=None,
  167. run_mode="single",
  168. shuffle=False
  169. ):
  170. """
  171. Runs multiple .thr files in sequence with options for pausing, clearing, shuffling, and looping.
  172. Parameters:
  173. - file_paths (list): List of file paths to run.
  174. - pause_time (float): Seconds to pause between patterns.
  175. - clear_pattern (str): Specific clear pattern to run ("clear_in", "clear_out", "clear_sideway", or "random").
  176. - run_mode (str): "single" for one-time run or "indefinite" for looping.
  177. - shuffle (bool): Whether to shuffle the playlist before running.
  178. """
  179. global stop_requested
  180. stop_requested = False # Reset stop flag at the start
  181. if shuffle:
  182. random.shuffle(file_paths)
  183. print("Playlist shuffled.")
  184. while True:
  185. for idx, path in enumerate(file_paths):
  186. if stop_requested:
  187. print("Execution stopped before starting next pattern.")
  188. return
  189. # Run the main pattern
  190. print(f"Running pattern {idx + 1} of {len(file_paths)}: {path}")
  191. run_theta_rho_file(path)
  192. if idx < len(file_paths) -1:
  193. if stop_requested:
  194. print("Execution stopped before running the next clear pattern.")
  195. return
  196. # Pause after each pattern if requested
  197. if pause_time > 0:
  198. print(f"Pausing for {pause_time} seconds...")
  199. time.sleep(pause_time)
  200. if clear_pattern:
  201. if stop_requested:
  202. print("Execution stopped before running the next clear pattern.")
  203. return
  204. # Determine the clear pattern to run
  205. clear_file_path = get_clear_pattern_file(clear_pattern)
  206. print(f"Running clear pattern: {clear_file_path}")
  207. run_theta_rho_file(clear_file_path)
  208. # After completing the playlist
  209. if run_mode == "indefinite":
  210. print("Playlist completed. Restarting as per 'indefinite' run mode.")
  211. if pause_time > 0:
  212. print(f"Pausing for {pause_time} seconds before restarting...")
  213. time.sleep(pause_time)
  214. if shuffle:
  215. random.shuffle(file_paths)
  216. print("Playlist reshuffled for the next loop.")
  217. continue
  218. else:
  219. print("Playlist completed.")
  220. break
  221. # Reset theta after execution or stopping
  222. reset_theta()
  223. ser.write("FINISHED\n".encode())
  224. print("All requested patterns completed (or stopped).")
  225. def reset_theta():
  226. """Reset theta on the Arduino."""
  227. ser.write("RESET_THETA\n".encode())
  228. while True:
  229. with serial_lock:
  230. if ser.in_waiting > 0:
  231. response = ser.readline().decode().strip()
  232. print(f"Arduino response: {response}")
  233. if response == "THETA_RESET":
  234. print("Theta successfully reset.")
  235. break
  236. time.sleep(0.5) # Small delay to avoid busy waiting
  237. # Flask API Endpoints
  238. @app.route('/')
  239. def index():
  240. return render_template('index.html')
  241. @app.route('/list_serial_ports', methods=['GET'])
  242. def list_ports():
  243. return jsonify(list_serial_ports())
  244. @app.route('/connect_serial', methods=['POST'])
  245. def connect_serial():
  246. port = request.json.get('port')
  247. if not port:
  248. return jsonify({'error': 'No port provided'}), 400
  249. try:
  250. connect_to_serial(port)
  251. return jsonify({'success': True})
  252. except Exception as e:
  253. return jsonify({'error': str(e)}), 500
  254. @app.route('/disconnect_serial', methods=['POST'])
  255. def disconnect():
  256. try:
  257. disconnect_serial()
  258. return jsonify({'success': True})
  259. except Exception as e:
  260. return jsonify({'error': str(e)}), 500
  261. @app.route('/restart_serial', methods=['POST'])
  262. def restart():
  263. port = request.json.get('port')
  264. if not port:
  265. return jsonify({'error': 'No port provided'}), 400
  266. try:
  267. restart_serial(port)
  268. return jsonify({'success': True})
  269. except Exception as e:
  270. return jsonify({'error': str(e)}), 500
  271. @app.route('/list_theta_rho_files', methods=['GET'])
  272. def list_theta_rho_files():
  273. files = []
  274. for root, _, filenames in os.walk(THETA_RHO_DIR):
  275. for file in filenames:
  276. # Construct the relative file path
  277. relative_path = os.path.relpath(os.path.join(root, file), THETA_RHO_DIR)
  278. files.append(relative_path)
  279. return jsonify(sorted(files))
  280. @app.route('/upload_theta_rho', methods=['POST'])
  281. def upload_theta_rho():
  282. custom_patterns_dir = os.path.join(THETA_RHO_DIR, 'custom_patterns')
  283. os.makedirs(custom_patterns_dir, exist_ok=True) # Ensure the directory exists
  284. file = request.files['file']
  285. if file:
  286. file.save(os.path.join(custom_patterns_dir, file.filename))
  287. return jsonify({'success': True})
  288. return jsonify({'success': False})
  289. @app.route('/run_theta_rho', methods=['POST'])
  290. def run_theta_rho():
  291. file_name = request.json.get('file_name')
  292. pre_execution = request.json.get('pre_execution') # 'clear_in', 'clear_out', or 'none'
  293. if not file_name:
  294. return jsonify({'error': 'No file name provided'}), 400
  295. file_path = os.path.join(THETA_RHO_DIR, file_name)
  296. if not os.path.exists(file_path):
  297. return jsonify({'error': 'File not found'}), 404
  298. try:
  299. # Build a list of files to run in sequence
  300. files_to_run = []
  301. if pre_execution == 'clear_in':
  302. files_to_run.append('./patterns/clear_from_in.thr')
  303. elif pre_execution == 'clear_out':
  304. files_to_run.append('./patterns/clear_from_out.thr')
  305. elif pre_execution == 'clear_sideway':
  306. files_to_run.append('./patterns/clear_sideway.thr')
  307. elif pre_execution == 'none':
  308. pass # No pre-execution action required
  309. # Finally, add the main file
  310. files_to_run.append(file_path)
  311. # Run them in one shot using run_theta_rho_files (blocking call)
  312. threading.Thread(
  313. target=run_theta_rho_files,
  314. args=(files_to_run,),
  315. kwargs={
  316. 'pause_time': 0,
  317. 'clear_pattern': None
  318. }
  319. ).start()
  320. return jsonify({'success': True})
  321. except Exception as e:
  322. return jsonify({'error': str(e)}), 500
  323. @app.route('/stop_execution', methods=['POST'])
  324. def stop_execution():
  325. global stop_requested
  326. stop_requested = True
  327. return jsonify({'success': True})
  328. @app.route('/send_home', methods=['POST'])
  329. def send_home():
  330. """Send the HOME command to the Arduino."""
  331. try:
  332. send_command("HOME")
  333. return jsonify({'success': True})
  334. except Exception as e:
  335. return jsonify({'error': str(e)}), 500
  336. @app.route('/run_theta_rho_file/<file_name>', methods=['POST'])
  337. def run_specific_theta_rho_file(file_name):
  338. """Run a specific theta-rho file."""
  339. file_path = os.path.join(THETA_RHO_DIR, file_name)
  340. if not os.path.exists(file_path):
  341. return jsonify({'error': 'File not found'}), 404
  342. threading.Thread(target=run_theta_rho_file, args=(file_path,)).start()
  343. return jsonify({'success': True})
  344. @app.route('/delete_theta_rho_file', methods=['POST'])
  345. def delete_theta_rho_file():
  346. data = request.json
  347. file_name = data.get('file_name')
  348. if not file_name:
  349. return jsonify({"success": False, "error": "No file name provided"}), 400
  350. file_path = os.path.join(THETA_RHO_DIR, file_name)
  351. if not os.path.exists(file_path):
  352. return jsonify({"success": False, "error": "File not found"}), 404
  353. try:
  354. os.remove(file_path)
  355. return jsonify({"success": True})
  356. except Exception as e:
  357. return jsonify({"success": False, "error": str(e)}), 500
  358. @app.route('/move_to_center', methods=['POST'])
  359. def move_to_center():
  360. """Move the sand table to the center position."""
  361. try:
  362. if ser is None or not ser.is_open:
  363. return jsonify({"success": False, "error": "Serial connection not established"}), 400
  364. coordinates = [(0, 0)] # Center position
  365. send_coordinate_batch(ser, coordinates)
  366. return jsonify({"success": True})
  367. except Exception as e:
  368. return jsonify({"success": False, "error": str(e)}), 500
  369. @app.route('/move_to_perimeter', methods=['POST'])
  370. def move_to_perimeter():
  371. """Move the sand table to the perimeter position."""
  372. try:
  373. if ser is None or not ser.is_open:
  374. return jsonify({"success": False, "error": "Serial connection not established"}), 400
  375. MAX_RHO = 1
  376. coordinates = [(0, MAX_RHO)] # Perimeter position
  377. send_coordinate_batch(ser, coordinates)
  378. return jsonify({"success": True})
  379. except Exception as e:
  380. return jsonify({"success": False, "error": str(e)}), 500
  381. @app.route('/preview_thr', methods=['POST'])
  382. def preview_thr():
  383. file_name = request.json.get('file_name')
  384. if not file_name:
  385. return jsonify({'error': 'No file name provided'}), 400
  386. file_path = os.path.join(THETA_RHO_DIR, file_name)
  387. if not os.path.exists(file_path):
  388. return jsonify({'error': 'File not found'}), 404
  389. try:
  390. # Parse the .thr file with transformations
  391. coordinates = parse_theta_rho_file(file_path)
  392. return jsonify({'success': True, 'coordinates': coordinates})
  393. except Exception as e:
  394. return jsonify({'error': str(e)}), 500
  395. @app.route('/send_coordinate', methods=['POST'])
  396. def send_coordinate():
  397. """Send a single (theta, rho) coordinate to the Arduino."""
  398. global ser
  399. if ser is None or not ser.is_open:
  400. return jsonify({"success": False, "error": "Serial connection not established"}), 400
  401. try:
  402. data = request.json
  403. theta = data.get('theta')
  404. rho = data.get('rho')
  405. if theta is None or rho is None:
  406. return jsonify({"success": False, "error": "Theta and Rho are required"}), 400
  407. # Send the coordinate to the Arduino
  408. send_coordinate_batch(ser, [(theta, rho)])
  409. reset_theta()
  410. return jsonify({"success": True})
  411. except Exception as e:
  412. return jsonify({"success": False, "error": str(e)}), 500
  413. # Expose files for download if needed
  414. @app.route('/download/<filename>', methods=['GET'])
  415. def download_file(filename):
  416. """Download a file from the theta-rho directory."""
  417. return send_from_directory(THETA_RHO_DIR, filename)
  418. @app.route('/serial_status', methods=['GET'])
  419. def serial_status():
  420. global ser, ser_port
  421. return jsonify({
  422. 'connected': ser.is_open if ser else False,
  423. 'port': ser_port # Include the port name
  424. })
  425. if not os.path.exists(PLAYLISTS_FILE):
  426. with open(PLAYLISTS_FILE, "w") as f:
  427. json.dump({}, f, indent=2)
  428. def load_playlists():
  429. """
  430. Load the entire playlists dictionary from the JSON file.
  431. Returns something like: {
  432. "My Playlist": ["file1.thr", "file2.thr"],
  433. "Another": ["x.thr"]
  434. }
  435. """
  436. with open(PLAYLISTS_FILE, "r") as f:
  437. return json.load(f)
  438. def save_playlists(playlists_dict):
  439. """
  440. Save the entire playlists dictionary back to the JSON file.
  441. """
  442. with open(PLAYLISTS_FILE, "w") as f:
  443. json.dump(playlists_dict, f, indent=2)
  444. @app.route("/list_all_playlists", methods=["GET"])
  445. def list_all_playlists():
  446. """
  447. Returns a list of all playlist names.
  448. Example return: ["My Playlist", "Another Playlist"]
  449. """
  450. playlists_dict = load_playlists()
  451. playlist_names = list(playlists_dict.keys())
  452. return jsonify(playlist_names)
  453. @app.route("/get_playlist", methods=["GET"])
  454. def get_playlist():
  455. """
  456. GET /get_playlist?name=My%20Playlist
  457. Returns: { "name": "My Playlist", "files": [... ] }
  458. """
  459. playlist_name = request.args.get("name", "")
  460. if not playlist_name:
  461. return jsonify({"error": "Missing playlist 'name' parameter"}), 400
  462. playlists_dict = load_playlists()
  463. if playlist_name not in playlists_dict:
  464. return jsonify({"error": f"Playlist '{playlist_name}' not found"}), 404
  465. files = playlists_dict[playlist_name] # e.g. ["file1.thr", "file2.thr"]
  466. return jsonify({
  467. "name": playlist_name,
  468. "files": files
  469. })
  470. @app.route("/create_playlist", methods=["POST"])
  471. def create_playlist():
  472. """
  473. POST /create_playlist
  474. Body: { "name": "My Playlist", "files": ["file1.thr", "file2.thr"] }
  475. Creates or overwrites a playlist with the given name.
  476. """
  477. data = request.get_json()
  478. if not data or "name" not in data or "files" not in data:
  479. return jsonify({"success": False, "error": "Playlist 'name' and 'files' are required"}), 400
  480. playlist_name = data["name"]
  481. files = data["files"]
  482. # Load all playlists
  483. playlists_dict = load_playlists()
  484. # Overwrite or create new
  485. playlists_dict[playlist_name] = files
  486. # Save changes
  487. save_playlists(playlists_dict)
  488. return jsonify({
  489. "success": True,
  490. "message": f"Playlist '{playlist_name}' created/updated"
  491. })
  492. @app.route("/modify_playlist", methods=["POST"])
  493. def modify_playlist():
  494. """
  495. POST /modify_playlist
  496. Body: { "name": "My Playlist", "files": ["file1.thr", "file2.thr"] }
  497. Updates (or creates) the existing playlist with a new file list.
  498. You can 404 if you only want to allow modifications to existing playlists.
  499. """
  500. data = request.get_json()
  501. if not data or "name" not in data or "files" not in data:
  502. return jsonify({"success": False, "error": "Playlist 'name' and 'files' are required"}), 400
  503. playlist_name = data["name"]
  504. files = data["files"]
  505. # Load all playlists
  506. playlists_dict = load_playlists()
  507. # Optional: If you want to disallow creating a new playlist here:
  508. # if playlist_name not in playlists_dict:
  509. # return jsonify({"success": False, "error": f"Playlist '{playlist_name}' not found"}), 404
  510. # Overwrite or create new
  511. playlists_dict[playlist_name] = files
  512. # Save
  513. save_playlists(playlists_dict)
  514. return jsonify({"success": True, "message": f"Playlist '{playlist_name}' updated"})
  515. @app.route("/delete_playlist", methods=["DELETE"])
  516. def delete_playlist():
  517. """
  518. DELETE /delete_playlist
  519. Body: { "name": "My Playlist" }
  520. Removes the playlist from the single JSON file.
  521. """
  522. data = request.get_json()
  523. if not data or "name" not in data:
  524. return jsonify({"success": False, "error": "Missing 'name' field"}), 400
  525. playlist_name = data["name"]
  526. playlists_dict = load_playlists()
  527. if playlist_name not in playlists_dict:
  528. return jsonify({"success": False, "error": f"Playlist '{playlist_name}' not found"}), 404
  529. # Remove from dict
  530. del playlists_dict[playlist_name]
  531. save_playlists(playlists_dict)
  532. return jsonify({
  533. "success": True,
  534. "message": f"Playlist '{playlist_name}' deleted"
  535. })
  536. @app.route("/run_playlist", methods=["POST"])
  537. def run_playlist():
  538. """
  539. POST /run_playlist
  540. Body (JSON):
  541. {
  542. "playlist_name": "My Playlist",
  543. "pause_time": 1.0, # Optional: seconds to pause between patterns
  544. "clear_pattern": "random", # Optional: "clear_in", "clear_out", "clear_sideway", or "random"
  545. "run_mode": "single", # 'single' or 'indefinite'
  546. "shuffle": True # true or false
  547. }
  548. """
  549. data = request.get_json()
  550. # Validate input
  551. if not data or "playlist_name" not in data:
  552. return jsonify({"success": False, "error": "Missing 'playlist_name' field"}), 400
  553. playlist_name = data["playlist_name"]
  554. pause_time = data.get("pause_time", 0)
  555. clear_pattern = data.get("clear_pattern", None)
  556. run_mode = data.get("run_mode", "single") # Default to 'single' run
  557. shuffle = data.get("shuffle", False) # Default to no shuffle
  558. # Validate pause_time
  559. if not isinstance(pause_time, (int, float)) or pause_time < 0:
  560. return jsonify({"success": False, "error": "'pause_time' must be a non-negative number"}), 400
  561. # Validate clear_pattern
  562. valid_patterns = ["clear_in", "clear_out", "clear_sideway", "random"]
  563. if clear_pattern not in valid_patterns:
  564. clear_pattern = None
  565. # Validate run_mode
  566. if run_mode not in ["single", "indefinite"]:
  567. return jsonify({"success": False, "error": "'run_mode' must be 'single' or 'indefinite'"}), 400
  568. # Validate shuffle
  569. if not isinstance(shuffle, bool):
  570. return jsonify({"success": False, "error": "'shuffle' must be a boolean value"}), 400
  571. # Load playlists
  572. playlists = load_playlists()
  573. if playlist_name not in playlists:
  574. return jsonify({"success": False, "error": f"Playlist '{playlist_name}' not found"}), 404
  575. file_paths = playlists[playlist_name]
  576. file_paths = [os.path.join(THETA_RHO_DIR, file) for file in file_paths]
  577. if not file_paths:
  578. return jsonify({"success": False, "error": f"Playlist '{playlist_name}' is empty"}), 400
  579. # Start the playlist execution in a separate thread
  580. try:
  581. threading.Thread(
  582. target=run_theta_rho_files,
  583. args=(file_paths,),
  584. kwargs={
  585. 'pause_time': pause_time,
  586. 'clear_pattern': clear_pattern,
  587. 'run_mode': run_mode,
  588. 'shuffle': shuffle
  589. },
  590. daemon=True # Daemonize thread to exit with the main program
  591. ).start()
  592. return jsonify({"success": True, "message": f"Playlist '{playlist_name}' is now running."})
  593. except Exception as e:
  594. return jsonify({"success": False, "error": str(e)}), 500
  595. if __name__ == '__main__':
  596. app.run(debug=True, host='0.0.0.0', port=8080)