app.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. from flask import Flask, request, jsonify, render_template, send_from_directory
  2. import atexit
  3. import os
  4. import logging
  5. from datetime import datetime
  6. from .modules.serial import serial_manager
  7. from dune_weaver_flask.modules.core import pattern_manager
  8. from dune_weaver_flask.modules.core import playlist_manager
  9. from .modules.firmware import firmware_manager
  10. from dune_weaver_flask.modules.core.state import state
  11. # Configure logging
  12. logging.basicConfig(
  13. level=logging.INFO,
  14. format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
  15. handlers=[
  16. logging.StreamHandler(),
  17. # disable file logging for now, to not gobble up resources
  18. # logging.FileHandler('dune_weaver.log')
  19. ]
  20. )
  21. logger = logging.getLogger(__name__)
  22. app = Flask(__name__)
  23. # Flask API Endpoints
  24. @app.route('/')
  25. def index():
  26. return render_template('index.html')
  27. @app.route('/list_serial_ports', methods=['GET'])
  28. def list_ports():
  29. logger.debug("Listing available serial ports")
  30. return jsonify(serial_manager.list_serial_ports())
  31. @app.route('/connect_serial', methods=['POST'])
  32. def connect_serial():
  33. port = request.json.get('port')
  34. if not port:
  35. logger.warning('Serial connection attempt without port specified')
  36. return jsonify({'error': 'No port provided'}), 400
  37. try:
  38. serial_manager.connect_to_serial(port)
  39. logger.info(f'Successfully connected to serial port {port}')
  40. return jsonify({'success': True})
  41. except Exception as e:
  42. logger.error(f'Failed to connect to serial port {port}: {str(e)}')
  43. return jsonify({'error': str(e)}), 500
  44. @app.route('/disconnect_serial', methods=['POST'])
  45. def disconnect():
  46. try:
  47. serial_manager.disconnect_serial()
  48. logger.info('Successfully disconnected from serial port')
  49. return jsonify({'success': True})
  50. except Exception as e:
  51. logger.error(f'Failed to disconnect serial: {str(e)}')
  52. return jsonify({'error': str(e)}), 500
  53. @app.route('/restart_serial', methods=['POST'])
  54. def restart():
  55. port = request.json.get('port')
  56. if not port:
  57. logger.warning("Restart serial request received without port")
  58. return jsonify({'error': 'No port provided'}), 400
  59. try:
  60. logger.info(f"Restarting serial connection on port {port}")
  61. serial_manager.restart_serial(port)
  62. return jsonify({'success': True})
  63. except Exception as e:
  64. logger.error(f"Failed to restart serial on port {port}: {str(e)}")
  65. return jsonify({'error': str(e)}), 500
  66. @app.route('/list_theta_rho_files', methods=['GET'])
  67. def list_theta_rho_files():
  68. logger.debug("Listing theta-rho files")
  69. files = pattern_manager.list_theta_rho_files()
  70. return jsonify(sorted(files))
  71. @app.route('/upload_theta_rho', methods=['POST'])
  72. def upload_theta_rho():
  73. custom_patterns_dir = os.path.join(pattern_manager.THETA_RHO_DIR, 'custom_patterns')
  74. os.makedirs(custom_patterns_dir, exist_ok=True)
  75. logger.debug(f'Ensuring custom patterns directory exists: {custom_patterns_dir}')
  76. file = request.files['file']
  77. if file:
  78. file_path = os.path.join(custom_patterns_dir, file.filename)
  79. file.save(file_path)
  80. logger.info(f'Successfully uploaded theta-rho file: {file.filename}')
  81. return jsonify({'success': True})
  82. logger.warning('Upload theta-rho request received without file')
  83. return jsonify({'success': False})
  84. @app.route('/run_theta_rho', methods=['POST'])
  85. def run_theta_rho():
  86. file_name = request.json.get('file_name')
  87. pre_execution = request.json.get('pre_execution')
  88. if not file_name:
  89. logger.warning('Run theta-rho request received without file name')
  90. return jsonify({'error': 'No file name provided'}), 400
  91. file_path = os.path.join(pattern_manager.THETA_RHO_DIR, file_name)
  92. if not os.path.exists(file_path):
  93. logger.error(f'Theta-rho file not found: {file_path}')
  94. return jsonify({'error': 'File not found'}), 404
  95. try:
  96. files_to_run = [file_path]
  97. logger.info(f'Running theta-rho file: {file_name} with pre_execution={pre_execution}')
  98. pattern_manager.run_theta_rho_files(files_to_run, clear_pattern=pre_execution)
  99. return jsonify({'success': True})
  100. except Exception as e:
  101. logger.error(f'Failed to run theta-rho file {file_name}: {str(e)}')
  102. return jsonify({'error': str(e)}), 500
  103. @app.route('/stop_execution', methods=['POST'])
  104. def stop_execution():
  105. pattern_manager.stop_actions()
  106. return jsonify({'success': True})
  107. @app.route('/send_home', methods=['POST'])
  108. def send_home():
  109. try:
  110. serial_manager.home()
  111. return jsonify({'success': True})
  112. except Exception as e:
  113. logger.error(f"Failed to send home command: {str(e)}")
  114. return jsonify({'error': str(e)}), 500
  115. @app.route('/run_theta_rho_file/<file_name>', methods=['POST'])
  116. def run_specific_theta_rho_file(file_name):
  117. file_path = os.path.join(pattern_manager.THETA_RHO_DIR, file_name)
  118. if not os.path.exists(file_path):
  119. return jsonify({'error': 'File not found'}), 404
  120. pattern_manager.run_theta_rho_file(file_path)
  121. return jsonify({'success': True})
  122. @app.route('/delete_theta_rho_file', methods=['POST'])
  123. def delete_theta_rho_file():
  124. file_name = request.json.get('file_name')
  125. if not file_name:
  126. logger.warning("Delete theta-rho file request received without filename")
  127. return jsonify({"success": False, "error": "No file name provided"}), 400
  128. file_path = os.path.join(pattern_manager.THETA_RHO_DIR, file_name)
  129. if not os.path.exists(file_path):
  130. logger.error(f"Attempted to delete non-existent file: {file_path}")
  131. return jsonify({"success": False, "error": "File not found"}), 404
  132. try:
  133. os.remove(file_path)
  134. logger.info(f"Successfully deleted theta-rho file: {file_name}")
  135. return jsonify({"success": True})
  136. except Exception as e:
  137. logger.error(f"Failed to delete theta-rho file {file_name}: {str(e)}")
  138. return jsonify({"success": False, "error": str(e)}), 500
  139. @app.route('/move_to_center', methods=['POST'])
  140. def move_to_center():
  141. global current_theta
  142. try:
  143. if not serial_manager.is_connected():
  144. logger.warning("Attempted to move to center without serial connection")
  145. return jsonify({"success": False, "error": "Serial connection not established"}), 400
  146. logger.info("Moving device to center position")
  147. pattern_manager.reset_theta()
  148. pattern_manager.move_polar(0, 0)
  149. return jsonify({"success": True})
  150. except Exception as e:
  151. logger.error(f"Failed to move to center: {str(e)}")
  152. return jsonify({"success": False, "error": str(e)}), 500
  153. @app.route('/move_to_perimeter', methods=['POST'])
  154. def move_to_perimeter():
  155. global current_theta
  156. try:
  157. if not serial_manager.is_connected():
  158. logger.warning("Attempted to move to perimeter without serial connection")
  159. return jsonify({"success": False, "error": "Serial connection not established"}), 400
  160. pattern_manager.reset_theta()
  161. pattern_manager.move_polar(0,1)
  162. return jsonify({"success": True})
  163. except Exception as e:
  164. logger.error(f"Failed to move to perimeter: {str(e)}")
  165. return jsonify({"success": False, "error": str(e)}), 500
  166. @app.route('/preview_thr', methods=['POST'])
  167. def preview_thr():
  168. file_name = request.json.get('file_name')
  169. if not file_name:
  170. logger.warning("Preview theta-rho request received without filename")
  171. return jsonify({'error': 'No file name provided'}), 400
  172. file_path = os.path.join(pattern_manager.THETA_RHO_DIR, file_name)
  173. if not os.path.exists(file_path):
  174. logger.error(f"Attempted to preview non-existent file: {file_path}")
  175. return jsonify({'error': 'File not found'}), 404
  176. try:
  177. coordinates = pattern_manager.parse_theta_rho_file(file_path)
  178. return jsonify({'success': True, 'coordinates': coordinates})
  179. except Exception as e:
  180. logger.error(f"Failed to generate preview for {file_name}: {str(e)}")
  181. return jsonify({'error': str(e)}), 500
  182. @app.route('/send_coordinate', methods=['POST'])
  183. def send_coordinate():
  184. if not serial_manager.is_connected():
  185. logger.warning("Attempted to send coordinate without serial connection")
  186. return jsonify({"success": False, "error": "Serial connection not established"}), 400
  187. try:
  188. data = request.json
  189. theta = data.get('theta')
  190. rho = data.get('rho')
  191. if theta is None or rho is None:
  192. logger.warning("Send coordinate request missing theta or rho values")
  193. return jsonify({"success": False, "error": "Theta and Rho are required"}), 400
  194. logger.debug(f"Sending coordinate: theta={theta}, rho={rho}")
  195. pattern_manager.move_polar(theta, rho)
  196. return jsonify({"success": True})
  197. except Exception as e:
  198. logger.error(f"Failed to send coordinate: {str(e)}")
  199. return jsonify({"success": False, "error": str(e)}), 500
  200. @app.route('/download/<filename>', methods=['GET'])
  201. def download_file(filename):
  202. return send_from_directory(pattern_manager.THETA_RHO_DIR, filename)
  203. @app.route('/serial_status', methods=['GET'])
  204. def serial_status():
  205. connected = serial_manager.is_connected()
  206. port = serial_manager.get_port()
  207. logger.debug(f"Serial status check - connected: {connected}, port: {port}")
  208. return jsonify({
  209. 'connected': connected,
  210. 'port': port
  211. })
  212. @app.route('/pause_execution', methods=['POST'])
  213. def pause_execution():
  214. logger.info("Pausing pattern execution")
  215. pattern_manager.pause_requested = True
  216. return jsonify({'success': True, 'message': 'Execution paused'})
  217. @app.route('/status', methods=['GET'])
  218. def get_status():
  219. return jsonify(pattern_manager.get_status())
  220. @app.route('/resume_execution', methods=['POST'])
  221. def resume_execution():
  222. logger.info("Resuming pattern execution")
  223. with pattern_manager.pause_condition:
  224. pattern_manager.pause_requested = False
  225. pattern_manager.pause_condition.notify_all()
  226. return jsonify({'success': True, 'message': 'Execution resumed'})
  227. # Playlist endpoints
  228. @app.route("/list_all_playlists", methods=["GET"])
  229. def list_all_playlists():
  230. playlist_names = playlist_manager.list_all_playlists()
  231. return jsonify(playlist_names)
  232. @app.route("/get_playlist", methods=["GET"])
  233. def get_playlist():
  234. playlist_name = request.args.get("name", "")
  235. if not playlist_name:
  236. return jsonify({"error": "Missing playlist 'name' parameter"}), 400
  237. playlist = playlist_manager.get_playlist(playlist_name)
  238. if not playlist:
  239. return jsonify({"error": f"Playlist '{playlist_name}' not found"}), 404
  240. return jsonify(playlist)
  241. @app.route("/create_playlist", methods=["POST"])
  242. def create_playlist():
  243. data = request.get_json()
  244. if not data or "name" not in data or "files" not in data:
  245. return jsonify({"success": False, "error": "Playlist 'name' and 'files' are required"}), 400
  246. success = playlist_manager.create_playlist(data["name"], data["files"])
  247. return jsonify({
  248. "success": success,
  249. "message": f"Playlist '{data['name']}' created/updated"
  250. })
  251. @app.route("/modify_playlist", methods=["POST"])
  252. def modify_playlist():
  253. data = request.get_json()
  254. if not data or "name" not in data or "files" not in data:
  255. return jsonify({"success": False, "error": "Playlist 'name' and 'files' are required"}), 400
  256. success = playlist_manager.modify_playlist(data["name"], data["files"])
  257. return jsonify({"success": success, "message": f"Playlist '{data['name']}' updated"})
  258. @app.route("/delete_playlist", methods=["DELETE"])
  259. def delete_playlist():
  260. data = request.get_json()
  261. if not data or "name" not in data:
  262. return jsonify({"success": False, "error": "Missing 'name' field"}), 400
  263. success = playlist_manager.delete_playlist(data["name"])
  264. if not success:
  265. return jsonify({"success": False, "error": f"Playlist '{data['name']}' not found"}), 404
  266. return jsonify({
  267. "success": True,
  268. "message": f"Playlist '{data['name']}' deleted"
  269. })
  270. @app.route('/add_to_playlist', methods=['POST'])
  271. def add_to_playlist():
  272. data = request.json
  273. playlist_name = data.get('playlist_name')
  274. pattern = data.get('pattern')
  275. success = playlist_manager.add_to_playlist(playlist_name, pattern)
  276. if not success:
  277. return jsonify(success=False, error='Playlist not found'), 404
  278. return jsonify(success=True)
  279. @app.route("/run_playlist", methods=["POST"])
  280. def run_playlist():
  281. data = request.get_json()
  282. if not data or "playlist_name" not in data:
  283. logger.warning("Run playlist request received without playlist name")
  284. return jsonify({"success": False, "error": "Missing 'playlist_name' field"}), 400
  285. playlist_name = data["playlist_name"]
  286. pause_time = data.get("pause_time", 0)
  287. clear_pattern = data.get("clear_pattern", None)
  288. run_mode = data.get("run_mode", "single")
  289. shuffle = data.get("shuffle", False)
  290. schedule_hours = None
  291. start_time = data.get("start_time")
  292. end_time = data.get("end_time")
  293. if start_time and end_time:
  294. try:
  295. start_time_obj = datetime.strptime(start_time, "%H:%M").time()
  296. end_time_obj = datetime.strptime(end_time, "%H:%M").time()
  297. if start_time_obj >= end_time_obj:
  298. logger.error(f"Invalid schedule times: start_time {start_time} >= end_time {end_time}")
  299. return jsonify({"success": False, "error": "'start_time' must be earlier than 'end_time'"}), 400
  300. schedule_hours = (start_time_obj, end_time_obj)
  301. logger.info(f"Playlist {playlist_name} scheduled to run between {start_time} and {end_time}")
  302. except ValueError:
  303. logger.error(f"Invalid time format provided: start_time={start_time}, end_time={end_time}")
  304. return jsonify({"success": False, "error": "Invalid time format. Use HH:MM (e.g., '09:30')"}), 400
  305. logger.info(f"Starting playlist '{playlist_name}' with mode={run_mode}, shuffle={shuffle}")
  306. success, message = playlist_manager.run_playlist(
  307. playlist_name,
  308. pause_time=pause_time,
  309. clear_pattern=clear_pattern,
  310. run_mode=run_mode,
  311. shuffle=shuffle,
  312. schedule_hours=schedule_hours
  313. )
  314. if not success:
  315. logger.error(f"Failed to run playlist '{playlist_name}': {message}")
  316. return jsonify({"success": False, "error": message}), 500
  317. return jsonify({"success": True, "message": message})
  318. # Firmware endpoints
  319. @app.route('/set_speed', methods=['POST'])
  320. def set_speed():
  321. try:
  322. data = request.json
  323. new_speed = data.get('speed')
  324. if new_speed is None:
  325. logger.warning("Set speed request received without speed value")
  326. return jsonify({"success": False, "error": "Speed is required"}), 400
  327. if not isinstance(new_speed, (int, float)) or new_speed <= 0:
  328. logger.warning(f"Invalid speed value received: {new_speed}")
  329. return jsonify({"success": False, "error": "Invalid speed value"}), 400
  330. state.speed = new_speed
  331. return jsonify({"success": True, "speed": new_speed})
  332. except Exception as e:
  333. logger.error(f"Failed to set speed: {str(e)}")
  334. return jsonify({"success": False, "error": str(e)}), 500
  335. @app.route('/check_software_update', methods=['GET'])
  336. def check_updates():
  337. update_info = firmware_manager.check_git_updates()
  338. return jsonify(update_info)
  339. @app.route('/update_software', methods=['POST'])
  340. def update_software():
  341. logger.info("Starting software update process")
  342. success, error_message, error_log = firmware_manager.update_software()
  343. if success:
  344. logger.info("Software update completed successfully")
  345. return jsonify({"success": True})
  346. else:
  347. logger.error(f"Software update failed: {error_message}\nDetails: {error_log}")
  348. return jsonify({
  349. "success": False,
  350. "error": error_message,
  351. "details": error_log
  352. }), 500
  353. def on_exit():
  354. """Function to execute on application shutdown."""
  355. pattern_manager.stop_actions()
  356. state.save()
  357. def entrypoint():
  358. logger.info("Starting Dune Weaver application...")
  359. # Register the on_exit function
  360. atexit.register(on_exit)
  361. # Auto-connect to serial
  362. try:
  363. serial_manager.connect_to_serial()
  364. except Exception as e:
  365. logger.warning(f"Failed to auto-connect to serial port: {str(e)}")
  366. try:
  367. logger.info("Starting Flask server on port 8080...")
  368. app.run(debug=False, host='0.0.0.0', port=8080)
  369. except KeyboardInterrupt:
  370. logger.info("Keyboard interrupt received. Shutting down.")
  371. except Exception as e:
  372. logger.critical(f"Unexpected error during server startup: {str(e)}")
  373. finally:
  374. on_exit()