app.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. from flask import Flask, request, jsonify, render_template, send_from_directory
  2. import atexit
  3. import os
  4. from datetime import datetime
  5. from .dune_weaver.serial.manager import serial_manager
  6. from .dune_weaver.core.pattern_manager.manager import pattern_manager
  7. from .dune_weaver.core.playlist_manager.manager import playlist_manager
  8. from .dune_weaver.firmware.manager import firmware_manager
  9. app = Flask(__name__)
  10. # Flask API Endpoints
  11. @app.route('/')
  12. def index():
  13. return render_template('index.html')
  14. @app.route('/list_serial_ports', methods=['GET'])
  15. def list_ports():
  16. return jsonify(serial_manager.list_serial_ports())
  17. @app.route('/connect_serial', methods=['POST'])
  18. def connect_serial():
  19. port = request.json.get('port')
  20. if not port:
  21. return jsonify({'error': 'No port provided'}), 400
  22. try:
  23. serial_manager.connect_to_serial(port)
  24. return jsonify({'success': True})
  25. except Exception as e:
  26. return jsonify({'error': str(e)}), 500
  27. @app.route('/disconnect_serial', methods=['POST'])
  28. def disconnect():
  29. try:
  30. serial_manager.disconnect_serial()
  31. return jsonify({'success': True})
  32. except Exception as e:
  33. return jsonify({'error': str(e)}), 500
  34. @app.route('/restart_serial', methods=['POST'])
  35. def restart():
  36. port = request.json.get('port')
  37. if not port:
  38. return jsonify({'error': 'No port provided'}), 400
  39. try:
  40. serial_manager.restart_serial(port)
  41. return jsonify({'success': True})
  42. except Exception as e:
  43. return jsonify({'error': str(e)}), 500
  44. @app.route('/list_theta_rho_files', methods=['GET'])
  45. def list_theta_rho_files():
  46. files = []
  47. for root, _, filenames in os.walk(pattern_manager.THETA_RHO_DIR):
  48. for file in filenames:
  49. relative_path = os.path.relpath(os.path.join(root, file), pattern_manager.THETA_RHO_DIR)
  50. files.append(relative_path)
  51. return jsonify(sorted(files))
  52. @app.route('/upload_theta_rho', methods=['POST'])
  53. def upload_theta_rho():
  54. custom_patterns_dir = os.path.join(pattern_manager.THETA_RHO_DIR, 'custom_patterns')
  55. os.makedirs(custom_patterns_dir, exist_ok=True)
  56. file = request.files['file']
  57. if file:
  58. file.save(os.path.join(custom_patterns_dir, file.filename))
  59. return jsonify({'success': True})
  60. return jsonify({'success': False})
  61. @app.route('/run_theta_rho', methods=['POST'])
  62. def run_theta_rho():
  63. file_name = request.json.get('file_name')
  64. pre_execution = request.json.get('pre_execution')
  65. if not file_name:
  66. return jsonify({'error': 'No file name provided'}), 400
  67. file_path = os.path.join(pattern_manager.THETA_RHO_DIR, file_name)
  68. if not os.path.exists(file_path):
  69. return jsonify({'error': 'File not found'}), 404
  70. try:
  71. files_to_run = [file_path]
  72. pattern_manager.run_theta_rho_files(files_to_run, clear_pattern=pre_execution)
  73. return jsonify({'success': True})
  74. except Exception as e:
  75. return jsonify({'error': str(e)}), 500
  76. @app.route('/stop_execution', methods=['POST'])
  77. def stop_execution():
  78. pattern_manager.stop_actions()
  79. return jsonify({'success': True})
  80. @app.route('/send_home', methods=['POST'])
  81. def send_home():
  82. try:
  83. serial_manager.send_command("HOME")
  84. return jsonify({'success': True})
  85. except Exception as e:
  86. return jsonify({'error': str(e)}), 500
  87. @app.route('/run_theta_rho_file/<file_name>', methods=['POST'])
  88. def run_specific_theta_rho_file(file_name):
  89. file_path = os.path.join(pattern_manager.THETA_RHO_DIR, file_name)
  90. if not os.path.exists(file_path):
  91. return jsonify({'error': 'File not found'}), 404
  92. pattern_manager.run_theta_rho_file(file_path)
  93. return jsonify({'success': True})
  94. @app.route('/delete_theta_rho_file', methods=['POST'])
  95. def delete_theta_rho_file():
  96. file_name = request.json.get('file_name')
  97. if not file_name:
  98. return jsonify({"success": False, "error": "No file name provided"}), 400
  99. file_path = os.path.join(pattern_manager.THETA_RHO_DIR, file_name)
  100. if not os.path.exists(file_path):
  101. return jsonify({"success": False, "error": "File not found"}), 404
  102. try:
  103. os.remove(file_path)
  104. return jsonify({"success": True})
  105. except Exception as e:
  106. return jsonify({"success": False, "error": str(e)}), 500
  107. @app.route('/move_to_center', methods=['POST'])
  108. def move_to_center():
  109. try:
  110. if not serial_manager.is_connected():
  111. return jsonify({"success": False, "error": "Serial connection not established"}), 400
  112. coordinates = [(0, 0)]
  113. serial_manager.send_coordinate_batch(coordinates)
  114. return jsonify({"success": True})
  115. except Exception as e:
  116. return jsonify({"success": False, "error": str(e)}), 500
  117. @app.route('/move_to_perimeter', methods=['POST'])
  118. def move_to_perimeter():
  119. try:
  120. if not serial_manager.is_connected():
  121. return jsonify({"success": False, "error": "Serial connection not established"}), 400
  122. MAX_RHO = 1
  123. coordinates = [(0, MAX_RHO)]
  124. serial_manager.send_coordinate_batch(coordinates)
  125. return jsonify({"success": True})
  126. except Exception as e:
  127. return jsonify({"success": False, "error": str(e)}), 500
  128. @app.route('/preview_thr', methods=['POST'])
  129. def preview_thr():
  130. file_name = request.json.get('file_name')
  131. if not file_name:
  132. return jsonify({'error': 'No file name provided'}), 400
  133. file_path = os.path.join(pattern_manager.THETA_RHO_DIR, file_name)
  134. if not os.path.exists(file_path):
  135. return jsonify({'error': 'File not found'}), 404
  136. try:
  137. coordinates = pattern_manager.parse_theta_rho_file(file_path)
  138. return jsonify({'success': True, 'coordinates': coordinates})
  139. except Exception as e:
  140. return jsonify({'error': str(e)}), 500
  141. @app.route('/send_coordinate', methods=['POST'])
  142. def send_coordinate():
  143. if not serial_manager.is_connected():
  144. return jsonify({"success": False, "error": "Serial connection not established"}), 400
  145. try:
  146. data = request.json
  147. theta = data.get('theta')
  148. rho = data.get('rho')
  149. if theta is None or rho is None:
  150. return jsonify({"success": False, "error": "Theta and Rho are required"}), 400
  151. serial_manager.send_coordinate_batch([(theta, rho)])
  152. return jsonify({"success": True})
  153. except Exception as e:
  154. return jsonify({"success": False, "error": str(e)}), 500
  155. @app.route('/download/<filename>', methods=['GET'])
  156. def download_file(filename):
  157. return send_from_directory(pattern_manager.THETA_RHO_DIR, filename)
  158. @app.route('/serial_status', methods=['GET'])
  159. def serial_status():
  160. return jsonify({
  161. 'connected': serial_manager.is_connected(),
  162. 'port': serial_manager.get_port()
  163. })
  164. @app.route('/pause_execution', methods=['POST'])
  165. def pause_execution():
  166. pattern_manager.pause_requested = True
  167. return jsonify({'success': True, 'message': 'Execution paused'})
  168. @app.route('/status', methods=['GET'])
  169. def get_status():
  170. return jsonify(pattern_manager.get_status())
  171. @app.route('/resume_execution', methods=['POST'])
  172. def resume_execution():
  173. with pattern_manager.pause_condition:
  174. pattern_manager.pause_requested = False
  175. pattern_manager.pause_condition.notify_all()
  176. return jsonify({'success': True, 'message': 'Execution resumed'})
  177. # Playlist endpoints
  178. @app.route("/list_all_playlists", methods=["GET"])
  179. def list_all_playlists():
  180. playlist_names = playlist_manager.list_all_playlists()
  181. return jsonify(playlist_names)
  182. @app.route("/get_playlist", methods=["GET"])
  183. def get_playlist():
  184. playlist_name = request.args.get("name", "")
  185. if not playlist_name:
  186. return jsonify({"error": "Missing playlist 'name' parameter"}), 400
  187. playlist = playlist_manager.get_playlist(playlist_name)
  188. if not playlist:
  189. return jsonify({"error": f"Playlist '{playlist_name}' not found"}), 404
  190. return jsonify(playlist)
  191. @app.route("/create_playlist", methods=["POST"])
  192. def create_playlist():
  193. data = request.get_json()
  194. if not data or "name" not in data or "files" not in data:
  195. return jsonify({"success": False, "error": "Playlist 'name' and 'files' are required"}), 400
  196. success = playlist_manager.create_playlist(data["name"], data["files"])
  197. return jsonify({
  198. "success": success,
  199. "message": f"Playlist '{data['name']}' created/updated"
  200. })
  201. @app.route("/modify_playlist", methods=["POST"])
  202. def modify_playlist():
  203. data = request.get_json()
  204. if not data or "name" not in data or "files" not in data:
  205. return jsonify({"success": False, "error": "Playlist 'name' and 'files' are required"}), 400
  206. success = playlist_manager.modify_playlist(data["name"], data["files"])
  207. return jsonify({"success": success, "message": f"Playlist '{data['name']}' updated"})
  208. @app.route("/delete_playlist", methods=["DELETE"])
  209. def delete_playlist():
  210. data = request.get_json()
  211. if not data or "name" not in data:
  212. return jsonify({"success": False, "error": "Missing 'name' field"}), 400
  213. success = playlist_manager.delete_playlist(data["name"])
  214. if not success:
  215. return jsonify({"success": False, "error": f"Playlist '{data['name']}' not found"}), 404
  216. return jsonify({
  217. "success": True,
  218. "message": f"Playlist '{data['name']}' deleted"
  219. })
  220. @app.route('/add_to_playlist', methods=['POST'])
  221. def add_to_playlist():
  222. data = request.json
  223. playlist_name = data.get('playlist_name')
  224. pattern = data.get('pattern')
  225. success = playlist_manager.add_to_playlist(playlist_name, pattern)
  226. if not success:
  227. return jsonify(success=False, error='Playlist not found'), 404
  228. return jsonify(success=True)
  229. @app.route("/run_playlist", methods=["POST"])
  230. def run_playlist():
  231. data = request.get_json()
  232. if not data or "playlist_name" not in data:
  233. return jsonify({"success": False, "error": "Missing 'playlist_name' field"}), 400
  234. playlist_name = data["playlist_name"]
  235. pause_time = data.get("pause_time", 0)
  236. clear_pattern = data.get("clear_pattern", None)
  237. run_mode = data.get("run_mode", "single")
  238. shuffle = data.get("shuffle", False)
  239. schedule_hours = None
  240. start_time = data.get("start_time")
  241. end_time = data.get("end_time")
  242. if start_time and end_time:
  243. try:
  244. start_time_obj = datetime.strptime(start_time, "%H:%M").time()
  245. end_time_obj = datetime.strptime(end_time, "%H:%M").time()
  246. if start_time_obj >= end_time_obj:
  247. return jsonify({"success": False, "error": "'start_time' must be earlier than 'end_time'"}), 400
  248. schedule_hours = (start_time_obj, end_time_obj)
  249. except ValueError:
  250. return jsonify({"success": False, "error": "Invalid time format. Use HH:MM (e.g., '09:30')"}), 400
  251. success, message = playlist_manager.run_playlist(
  252. playlist_name,
  253. pause_time=pause_time,
  254. clear_pattern=clear_pattern,
  255. run_mode=run_mode,
  256. shuffle=shuffle,
  257. schedule_hours=schedule_hours
  258. )
  259. if not success:
  260. return jsonify({"success": False, "error": message}), 500
  261. return jsonify({"success": True, "message": message})
  262. # Firmware endpoints
  263. @app.route('/set_speed', methods=['POST'])
  264. def set_speed():
  265. if not serial_manager.is_connected():
  266. return jsonify({"success": False, "error": "Serial connection not established"}), 400
  267. try:
  268. data = request.json
  269. speed = data.get('speed')
  270. if speed is None:
  271. return jsonify({"success": False, "error": "Speed is required"}), 400
  272. if not isinstance(speed, (int, float)) or speed <= 0:
  273. return jsonify({"success": False, "error": "Invalid speed value"}), 400
  274. serial_manager.send_command(f"SET_SPEED {speed}")
  275. return jsonify({"success": True, "speed": speed})
  276. except Exception as e:
  277. return jsonify({"success": False, "error": str(e)}), 500
  278. @app.route('/get_firmware_info', methods=['GET', 'POST'])
  279. def get_firmware_info():
  280. if not serial_manager.is_connected():
  281. return jsonify({"success": False, "error": "Arduino not connected or serial port not open"}), 400
  282. try:
  283. if request.method == "POST":
  284. motor_type = request.json.get("motorType", None)
  285. success, result = firmware_manager.get_firmware_info(motor_type)
  286. else:
  287. success, result = firmware_manager.get_firmware_info()
  288. if not success:
  289. return jsonify({"success": False, "error": result}), 500
  290. return jsonify({"success": True, **result})
  291. except Exception as e:
  292. return jsonify({"success": False, "error": str(e)}), 500
  293. @app.route('/flash_firmware', methods=['POST'])
  294. def flash_firmware():
  295. try:
  296. motor_type = request.json.get("motorType", None)
  297. success, message = firmware_manager.flash_firmware(motor_type)
  298. if not success:
  299. return jsonify({"success": False, "error": message}), 500
  300. return jsonify({"success": True, "message": message})
  301. except Exception as e:
  302. return jsonify({"success": False, "error": str(e)}), 500
  303. @app.route('/check_software_update', methods=['GET'])
  304. def check_updates():
  305. update_info = firmware_manager.check_git_updates()
  306. return jsonify(update_info)
  307. @app.route('/update_software', methods=['POST'])
  308. def update_software():
  309. success, error_message, error_log = firmware_manager.update_software()
  310. if success:
  311. return jsonify({"success": True})
  312. else:
  313. return jsonify({
  314. "success": False,
  315. "error": error_message,
  316. "details": error_log
  317. }), 500
  318. def on_exit():
  319. """Function to execute on application shutdown."""
  320. print("Shutting down the application...")
  321. pattern_manager.stop_actions()
  322. print("Execution stopped and resources cleaned up.")
  323. # Register the on_exit function
  324. atexit.register(on_exit)
  325. def entrypoint():
  326. # Auto-connect to serial
  327. serial_manager.connect_to_serial()
  328. try:
  329. app.run(debug=False, host='0.0.0.0', port=8080)
  330. except KeyboardInterrupt:
  331. print("Keyboard interrupt received. Shutting down.")
  332. finally:
  333. on_exit()