app.py 26 KB

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