import logging from flask import Flask, request, jsonify, Response, stream_with_context from flask_cors import CORS from dotenv import load_dotenv import os import subprocess import requests app = Flask(__name__) load_dotenv('/home/gds/printer/.env') # .env 파일 경로 CORS(app, resources={r"/*": {"origins": "*"}}) # 모든 출처에 대해 CORS를 활성화합니다. # 로그 디렉토리와 파일 설정 log_dir = '/home/gds/printer/log' if not os.path.exists(log_dir): os.makedirs(log_dir) log_file = os.path.join(log_dir, 'app.log') # 로깅 설정 logging.basicConfig(filename=log_file, level=logging.DEBUG, format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s') app.logger.addHandler(logging.StreamHandler()) app.logger.setLevel(logging.DEBUG) # start_gcode, end_gcode 변수 선언 start_gcode = """ M17 M862.1 P[nozzle_diameter] M862.3 P "MK4" M862.5 P2 M862.6 P "Input shaper" M115 U6.0.1+14848 M555 X{(min(print_bed_max[0], first_layer_print_min[0] + 32) - 32)} Y{(max(0, first_layer_print_min[1]) - 4)} W{((min(print_bed_max[0], max(first_layer_print_min[0] + 32, first_layer_print_max[0])))) - ((min(print_bed_max[0], first_layer_print_min[0] + 32) - 32))} H{((first_layer_print_max[1])) - ((max(0, first_layer_print_min[1]) - 4))} G90 M83 M140 S[first_layer_bed_temperature] {if filament_notes[0]=~/.*HT_MBL10.*/} M104 T0 S{first_layer_temperature[0] - 10} M109 T0 R{first_layer_temperature[0] - 10} {endif} {if filament_type[0] == "PC" or filament_type[0] == "PA"} M104 T0 S{first_layer_temperature[0] - 25} M109 T0 R{first_layer_temperature[0] - 25} {endif} {if filament_type[0] == "FLEX"} M104 T0 S210 M109 T0 R210 {endif} {if filament_type[0]=~/.*PET.*/} M104 T0 S175 M109 T0 R175 {endif} {if not (filament_notes[0]=~/.*HT_MBL10.*/ or filament_type[0] == "PC" or filament_type[0] == "PA" or filament_type[0] == "FLEX" or filament_type[0]=~/.*PET.*/)} M104 T0 S170 M109 T0 R170 {endif} M84 E G28 G1 X{10 + 32} Y-4 Z5 F4800 M302 S160 {if filament_type[initial_tool]=="FLEX"} G1 E-4 F2400 {else} G1 E-2 F2400 {endif} M84 E G29 P9 X10 Y-4 W32 H4 {if first_layer_bed_temperature[initial_tool]<=60}M106 S100{endif} G0 Z40 F10000 M190 S[first_layer_bed_temperature] M107 M84 E G29 P1 G29 P1 X0 Y0 W50 H20 C G29 P3.2 G29 P3.13 G29 A M104 S{first_layer_temperature[0]} G0 X0 Y-4 Z15 F4800 M109 S{first_layer_temperature[0]} G92 E0 M569 S0 E G92 E0 G1 E{(filament_type[0] == "FLEX" ? 4 : 2)} F2400 G0 E7 X15 Z0.2 F500 G0 X25 E4 F500 G0 X35 E4 F650 G0 X45 E4 F800 G0 X{45 + 3} Z{0.05} F{8000} G0 X{45 + 3 * 2} Z0.2 F{8000} G92 E0 M221 S100 """ end_gcode = """ {if layer_z < max_print_height}G1 Z{z_offset+min(layer_z+1, max_print_height)} F720{endif} M104 S0 M140 S0 M107 G1 X241 Y170 F3600 {if layer_z < max_print_height}G1 Z{z_offset+min(layer_z+23, max_print_height)} F300{endif} G4 M572 S0 M593 X T2 F0 M593 Y T2 F0 M84 X Y E """ error_code = '' err_code ='' def slice_stl(file_path, output_dir, options=None): """ STL 파일을 PrusaSlicer를 사용하여 슬라이스하는 함수 """ # PrusaSlicer 명령어 생성 prusa_slicer_path = '/usr/bin/PrusaSlicer' command = [ prusa_slicer_path, '-g', ] # 옵션 추가 for key, value in options.items(): command.extend([f'--{key}', value]) # 파일 경로 추가 command.append(f'"{file_path}"') # 출력 디렉토리는 PrusaSlicer가 인식할 수 있는 형식으로 설정 command.extend(['-o', output_dir]) # skirts가 0일 때 관련 옵션 제거 if options.get('skirts') == '0': command = [opt for idx, opt in enumerate(command) if 'skirt' not in opt and (idx == 0 or 'skirt' not in command[idx - 1])] command = [opt for idx, opt in enumerate(command) if 'skirts' not in opt and (idx == 0 or 'skirts' not in command[idx - 1])] # brim-type이 no_brim일 때 관련 옵션 제거 if options.get('brim_type') == 'no_brim': command = [opt for idx, opt in enumerate(command) if 'brim' not in opt and (idx == 0 or 'brim' not in command[idx - 1])] command = [opt for idx, opt in enumerate(command) if 'draft' not in opt and (idx == 0 or 'draft' not in command[idx - 1])] # 이 코드 처리 중에 --support-material 이나 --support-material-auto가 true,false에 따라서 다르게 동작해야 되는데, 둘중 하나 통과하는 부분에서 # command option이 사라진것 같아 확인 필요 i = 0 while i < len(command): if command[i] == '--support-material' and options.get('support_material') == 'true': command[i + 1] = '' # Set the next index to empty string i += 1 # Move to the next index to skip removing '--support-material' continue if command[i] == '--support-material-auto' and options.get('support_material_auto') == 'true': command[i + 1] = '' # Set the next index to empty string i += 1 # Move to the next index to skip removing '--support-material-auto' continue if command[i] == '--support-material' and options.get('support_material') == 'false': command.pop(i) # Remove '--support-material' command.pop(i) # Remove the value after '--support-material' continue if command[i] == '--support-material-auto' and options.get('support_material_auto') == 'false': command.pop(i) # Remove '--support-material-auto' command.pop(i) # Remove the value after '--support-material-auto' continue i += 1 #app.logger.debug("command : %s", ' '.join(command)) try: result = subprocess.run(' '.join(command), shell=True, capture_output=True, text=True, check=True) app.logger.debug("PrusaSlicer output: %s", result.stdout) return True except subprocess.CalledProcessError as e: app.logger.error("PrusaSlicer error: %s", e.stderr) global error_code error_code = e.stderr return False def parse_slicing_error(error_code): error_patterns = { "There is an object with no extrusions in the first layer.": (401, "첫 번째 레이어에 압출물이 없는 객체가 있습니다."), "No extrusions were generated for objects.": (402, "객체에 대한 압출물이 생성되지 않았습니다."), "The print is empty. The model is not printable with current print settings.": (403, "출력이 비어 있습니다. 현재 인쇄 설정으로는 모델을 인쇄할 수 없습니다."), "Levitating objects cannot be printed without supports.": (404, "떠있는 객체는 지지대 없이 인쇄할 수 없습니다."), "No layers were detected. You might want to repair your STL file(s) or check their size or thickness and retry.": (405, "레이어가 감지되지 않았습니다. STL 파일을 수리하거나 크기 또는 두께를 확인하고 다시 시도해 보십시오."), "No pad can be generated for this model with the current configuration": (406, "현재 설정으로는 이 모델에 대해 패드를 생성할 수 없습니다."), "There are unprintable objects. Try to adjust support settings to make the objects printable.": (407, "인쇄할 수 없는 객체가 있습니다. 객체를 인쇄 가능하게 만들기 위해 지지대 설정을 조정해 보십시오.") } for pattern, (code, message) in error_patterns.items(): if pattern in error_code: return code, message # 기본 에러 메시지 return 400, "모델 슬라이스에 실패했습니다." @app.route('/slice', methods=['POST']) def slice_model(): global error_code upload_dir = '/home/gds/printer/stl' if not os.path.exists(upload_dir): os.makedirs(upload_dir) # 1. file 없을 시 에러 발생 file = request.files.get('file') if not file: # STL 파일을 저장할 경로 설정 #2. file이 없어도 was에서 저장시킨 원래 파일명이 있을시 if 'orignlFileNm' in request.form and request.form['orignlFileNm']: file_path = os.path.join(upload_dir, request.form['orignlFileNm']) #3. 저장 경로 + 파일명이 존재하지 않을 시 if not os.path.exists(file_path): app.logger.error('No file in filepath') return jsonify({'err_code': '200','err_title':'경로상 파일 존재하지 않음','err_msg':'파일이 현재 경로 상에 존재하지 않습니다(/home/gds/printer)'}) else: app.logger.error('No file part') return jsonify({'err_code': '100','err_title':'업로드 파일 없음','err_msg':'업로드될 파일이 존재하지 않습니다.'}) else: file_extension = os.path.splitext(file.filename)[1].lower() if file_extension != '.stl': app.logger.error('Invalid file type: %s', file_extension) return jsonify({'err_code': '300','err_title':'확장자가 다름','err_msg':'STL 파일만 업로드 가능합니다.'}) file_path = os.path.join(upload_dir, file.filename) file.save(file_path) app.logger.debug('File saved to %s', file_path) #form으로 부터 dictionary 형태로 가지고 옴 options = request.form.to_dict() keys_to_remove = ['options_select_printer','options_select_filament', 'preset-options','wipe-tower-adjustment','menuId','orId','prId','atchFileId1','pageIndex','_csrf','orignlFileNm','atchFileId'] for key in keys_to_remove: if key in options: del options[key] options['filament-colour'] = '"'+options['filament-colour']+'"' options['fill-density'] = options['fill-density'] + "%" # 값이 "on"인 항목을 ""로 변경 for key, value in options.items(): if value == 'on' or value == 'true' or value == 'false' : options[key] = '' #start-gcode, end-gcode 추가 options['start-gcode'] = f"'{start_gcode}'" options['end-gcode'] = f"'{end_gcode}'" # PrusaSlicer를 사용하여 STL 파일 슬라이스 output_dir = '/home/gds/printer/gcode/' if not os.path.exists(output_dir): os.makedirs(output_dir) success = slice_stl(file_path, output_dir, options) if success: # G-code 파일 경로 if not file: gcode_file_path = os.path.join(output_dir,os.path.splitext(request.form['orignlFileNm'])[0]+ '.gcode') return jsonify({'gcode_file_path': gcode_file_path}) else: gcode_file_path = os.path.join(output_dir, os.path.splitext(file.filename)[0] + '.gcode') return jsonify({'gcode_file_path': gcode_file_path}) else: error_code, error_message = parse_slicing_error(error_code) return jsonify({'err_code': error_code,'err_title':'슬라이스 에러','err_msg': error_message}) @app.route('/getGCodeFile', methods=['GET']) def get_gcode_file(): gcode_file_path = request.args.get('gcodeFilePath') if not gcode_file_path: return jsonify({'error': 'Missing gcode_file_path parameter'}) try: with open(gcode_file_path, 'r') as file: gcode_string = file.read() return gcode_string except FileNotFoundError: return jsonify({'error': 'File not found'}) @app.route('/getPrusaConfig', methods=['GET']) def get_prusa_config(): config = { 'printer1': { 'url': os.getenv('PRUSA_PRINTER1_API_URL'), 'api_key': os.getenv('PRUSA_PRINTER1_API_KEY') }, 'printer2': { 'url': os.getenv('PRUSA_PRINTER2_API_URL'), 'api_key': os.getenv('PRUSA_PRINTER2_API_KEY') } } return jsonify(config) @app.route('/proxy', methods=['GET', 'POST', 'PUT', 'DELETE']) def proxy_prusa_link(): try: # 요청 URL과 파라미터 설정 url = request.args.get('url') if not url: return Response("URL is required", status=400) # PrusaLink에 보낼 요청 설정 headers = {key: value for key, value in request.headers.items() if key != 'Host'} response = requests.request( method=request.method, url=url, headers=headers, data=request.get_data(), cookies=request.cookies, stream=True # 스트리밍 응답 처리 ) # PrusaLink에서 받은 스트리밍 응답을 클라이언트에 전달 def generate(): for chunk in response.iter_content(chunk_size=8192): yield chunk proxy_response = Response(stream_with_context(generate()), status=response.status_code) for key, value in response.headers.items(): if key.lower() != 'content-encoding' and key.lower() != 'transfer-encoding': proxy_response.headers[key] = value return proxy_response except Exception as e: return Response(f"An error occurred: {str(e)}", status=500) if __name__ == '__main__': app.run(host='0.0.0.0', port=3000, debug=True)