diff --git a/api_routes.py b/api_routes.py index a1f6f85..14fba6d 100644 --- a/api_routes.py +++ b/api_routes.py @@ -1,3 +1,45 @@ +"""팔레타이징 로봇 제어 API (책 재단기 라인). + +[시스템 개요] + 재단기에서 나온 책 묶음이 컨베이어를 타고 '스태커(정렬대)'에 쌓이면, + JAKA 로봇이 포크형 그리퍼로 묶음을 떠서 팔레트 위에 층층이 적재한다. + 이 파일은 Flask Blueprint 로서 웹 UI(webview) 요청을 받아 아래 전부를 수행한다. + - 팔레트 종류별 적재 메인 루프 (wooden_wheel / iron_wheel / wooden_Flat / one_line_*) + - 로봇 상태 폴링 + 경광등/컨베이어 인터록 (get_status_thread) + - 스태커 실린더 시퀀스 제어 (stacker_thread, Modbus RTU) + +[로봇 제어박스 I/O 맵 - IO_CABINET] + DO0 : 그리퍼 수직 실린더(파지) 1=파지 / 0=해제 + DO1 : 그리퍼 수평 실린더(적재 푸셔) 1=전진 / 0=후퇴 + DO2 : 경광등 주황 (마지막 층 근접 예고) + DO3 : 경광등 초록 (정상 운전) + DO4 : 경광등 빨강 (정지/완료/비상) + DO5 : 동력장치(컨베이어) 운전 1=운전 / 0=정지 + DI0 : 스태커 수직 반사판 센서 (낱권 감지 → 스태커 1회 압축 동작 트리거) + DI1 : 스태커 책 준비 센서 (묶음 완성 → 로봇 픽업 조건) + DI2 : 스태커 수직 실린더 '위' 센서 + DI3 : 스태커 수직 실린더 '아래' 센서 (완전히 내려가야 로봇 진입 가능) + ※ DI 번호는 get_robot_status() 반환값의 din 배열 인덱스 기준. + +[Modbus RTU (COM3, slave 1) - 스태커 실린더] + coil 0 : 스태커 수직 실린더 (책 누름) + coil 1 : 스태커 수평 실린더 (책 밀어 정렬) + +[스레드 구조] + - Flask 요청 스레드 : /robot_start, /one_line_run 요청 안에서 적재 메인 루프가 + 그대로 돌기 때문에 해당 HTTP 요청은 작업 종료까지 블록된다. + - get_status_thread : 0.1s 주기로 로봇 상태/센서 폴링, 경광등·컨베이어 제어, + 비상정지(EMO) 해제 후 홈 복귀 처리. + - stacker_thread : 스태커 실린더로 책을 정렬/압축하는 반복 시퀀스. + +[진행 상태 저장] + - static/json/cache.json : 일반 적재 진행 상태 (몇 층/몇 권/현재 적재 높이) + - static/json/oneLine.json : 한줄쌓기 그리드별 진행 상태 + → 프로그램 재시작 시 이어서 적재할 수 있도록 매 권 적재 후 저장한다. + +[운영 로그] + - C:/palletizing_log/log_YYYYMMDD.txt (30일 경과분 자동 삭제) +""" from flask import Blueprint, request, jsonify, flash import time import json @@ -11,115 +53,141 @@ import re from datetime import datetime, timedelta import socket -log_folder_path = r"C:\palletizing_log" +log_folder_path = r"C:\palletizing_log" # 운영 로그 저장 폴더 +# JAKA SDK(jkrc.pyd)는 프로젝트 내 SDK 폴더에서 로드한다 current_dir = os.path.dirname(os.path.abspath(__file__)) sdk_path = os.path.join(current_dir, 'SDK') sys.path.insert(0, sdk_path) import jkrc -robot_ip_address = '10.5.5.100' -# robot_ip_address = '192.168.56.102' +robot_ip_address = '10.5.5.100' # 현장 로봇 컨트롤러 IP +# robot_ip_address = '192.168.56.102' # (테스트용 가상 컨트롤러 IP) robot = jkrc.RC(robot_ip_address) -IO_CABINET = 0 +IO_CABINET = 0 # jkrc IO 타입: 제어박스(캐비닛) IO PI = 3.1415926 api_routes = Blueprint('api_routes', __name__) +# ── 진행 상태 저장 파일 경로 ────────────────────────────────────────────── current_dir = os.path.dirname(os.path.abspath(__file__)) -cache_json_file_path = os.path.join(current_dir, 'static/json/cache.json') -oneLine_json_file_path = os.path.join(current_dir, 'static/json/oneLine.json') +cache_json_file_path = os.path.join(current_dir, 'static/json/cache.json') # 일반 적재 진행 상태 +oneLine_json_file_path = os.path.join(current_dir, 'static/json/oneLine.json') # 한줄쌓기 진행 상태 -wooden_wheel_pallet_height = 653 -iron_wheel_pallet_height = 633 +# ── 팔레트 치수/판별 기준 (mm) ──────────────────────────────────────────── +wooden_wheel_pallet_height = 653 # 나무 바퀴 팔레트 높이. UI 선택값과 비교해 종류 판별에도 사용 +iron_wheel_pallet_height = 633 # 철제 바퀴 팔레트 높이 -pallet_width = 0 +pallet_width = 0 # /update_pallet 에서 UI 입력값으로 갱신됨 pallet_length = 0 pallet_height = 0 -book_center_positions = [] +book_center_positions = [] # 그리드별 책 중심 좌표 목록 (UI에서 전달) -grid_id = 0 +grid_id = 0 # 한줄쌓기에서 선택된 그리드 ("grid1"~"grid4") -wheel_max_current_layer_height = 720 # 현재 레이어 최대 적재 높이 - 120 mm -flat_max_current_layer_height = 1080 # 현재 레이어 최대 적재 높이 - 120 mm +# 최대 적재 높이 판정 기준 (책 1층 = 130mm 두께로 계산) +wheel_max_current_layer_height = 720 # 바퀴 팔레트: 이 높이 초과 적재 불가 +flat_max_current_layer_height = 1080 # 납작 팔레트 +oneline_wheel_max_current_layer_height = 720 # 한줄쌓기 -oneline_wheel_max_current_layer_height = 720 +# ── 동작 상태 플래그 (스레드 간 공유 전역) ──────────────────────────────── +robot_start_clicked = False # UI '시작' 눌림 여부 (경광등 초록 조건) +terminate_robot_button_clicked = False # 작업 종료(완료/정지) 상태 +stacker_thread_running = False # 스태커 스레드 동작 여부 -robot_start_clicked = False -terminate_robot_button_clicked = False -stacker_thread_running = False +stacker_thread_operator = None # 스태커 스레드 핸들 (종료 시 join 용) +action = None # UI '로봇 동작 종료' 명령 ('robot_finish_action') +immediately_take_book = None # UI '즉시 픽업' 명령 (책 준비 센서 무시하고 픽업) +is_one_line_stop_clicked = None # UI '한줄쌓기 멈춤' 명령 +btn_id = None # 'normalButton'(일반 적재) / 'oneLineButton'(한줄쌓기) +flag = 0 # 비상정지 발생 이력 (1이면 해제 시 홈 복귀 필요) +is_emo_disabled = True # True = 비상정지 아님(정상). EMO 복귀 완료 시 True 복원 +# ── 모션 파라미터 ──────────────────────────────────────────────────────── +# ※ SDK 단위: 직선 이동 speed=mm/s, acc=mm/s² / 관절 이동 speed=rad/s, acc=rad/s² +# 아래 값은 컨트롤러 상한을 넘으므로 사실상 "허용 최대 속도"로 동작한다. +speed = 7000 # 직선 이동 속도 (책 리프트/적재 하강 등) +acc = 2300 # 직선 이동 가속도 -stacker_thread_operator = None -action = None -immediately_take_book = None - -is_one_line_stop_clicked = None - -btn_id = None - -flag = 0 # 비상정지버튼 눌렸는지 안눌렸는지에 대한 플래그 - -speed = 7000 -acc = 2300 - +# 그리드 진입(책 실은 채 적재 지점 상공으로 가는) 관절 이동 파라미터. +# acc=1(rad/s²)로 가감속을 매우 완만하게 하여 책 미끄러짐을 방지한다. +# ★ 택타임 튜닝 포인트: 책이 안 미끄러지는 범위에서 grid_acc를 올리면 사이클 단축 가능. grid_speed = 10 grid_acc = 1 -is_in_pose_time = 0.1 - -is_emo_disabled = True - -save_time = 2 - -stacker_vertical_cyl_sensor = 0 -stacker_vertical_cyl_down_sensor = 0 - -stacker_book_ready_sensor = 0 +# ═════════════════ 대기시간 설정 (택타임 튜닝 포인트) ═════════════════════ +# 값을 줄이면 사이클이 빨라지지만, 실린더 스트로크가 끝나기 전에 다음 동작이 +# 시작되면 책 낙하/밀림이 생길 수 있다. 문제 발생 시 주석의 [기존값]으로 복원할 것. +GRIPPER_RETRACT_WAIT = 0.5 # [기존 2.0] 픽업 진입 전 그리퍼 실린더 후퇴 확보 대기. + # 직전 사이클부터 이미 OFF 상태로 수 초간 이동해 왔으므로 + # 재확인 성격. 문제 발생 시 1.0 → 2.0 순으로 복원. +GRIP_CLAMP_WAIT_ODD = 0.3 # [기존 0.3] 홀수층 파지: 수직 그리퍼 ON 후 파지 완료 대기. +GRIP_CLAMP_WAIT_EVEN = 0.1 # [기존 0.1] 짝수층 파지: 〃. 짝수층은 0.1로 운영 검증되어 + # 있으므로 홀수층(0.3)도 단계적으로 줄여볼 여지 있음. +BOOK_SETTLE_WAIT = 0.3 # [기존 0.3] 책 들어올린 직후 흔들림 안정화 대기. +RELEASE_WAIT = 0.2 # [기존 0.3/0.2] 적재 후 수직 그리퍼 OFF→후퇴 대기 (0.2로 통일). +PUSH_HOLD_WAIT = 0.6 # [기존 1.0] 수평 푸셔 ON 후 스트로크 완료 대기. +FORK_EXIT_WAIT = 0.3 # [기존 0.5] 포크 인출(-180mm) 후 푸셔 해제 전 대기. +IN_POS_GUARD_TIME = 0.05 # [기존 0.1] 모션 명령 직후 in-pos 오판(이전 위치 기준 1 반환) + # 방지 가드. 책 1권당 8~10회 호출되므로 누적 영향이 큼. +IN_POS_POLL_INTERVAL = 0.01 # [신규] is_in_pos() 폴링 주기 (기존: 간격 없는 busy-wait) +IDLE_LOOP_INTERVAL = 0.02 # [신규] 메인 루프 유휴 폴링 주기 (기존: 간격 없는 busy-wait) +JKS_POLL_INTERVAL = 0.05 # [신규] 높이탐색 JKS 프로그램 종료 폴링 주기 +# ── 스태커 실린더 시퀀스 대기 (물리 스트로크 시간, 기존값 유지) ── +STACKER_BACKOFF = 0.5 # [기존 2.0] 책 준비 완료 상태에서 스태커 대기(재확인) 주기 +STACKER_PRE_PRESS_WAIT = 0.5 # 낱권 감지 후 수직 누름 시작 전 대기 +STACKER_PRESS_SETTLE = 0.05 # 수직 누름 후 수평 후퇴 시작 전 대기 +STACKER_SIDE_RETRACT = 1.2 # 수평 실린더 후퇴 유지(다음 책 유입 공간 확보) 시간 +STACKER_SIDE_PUSH = 0.2 # 수평 재전진 후 수직 해제 전 대기 +# ═══════════════════════════════════════════════════════════════════════ +# ── 스태커 센서 상태 (get_status_thread 가 0.1s 주기로 갱신) ────────────── +stacker_vertical_cyl_sensor = 0 # DI2: 수직 실린더 '위' 센서 +stacker_vertical_cyl_down_sensor = 0 # DI3: 수직 실린더 '아래' 센서 +stacker_book_ready_sensor = 0 # DI1: 책 묶음 준비 센서 +# ── 팔레트별 최대 하강 z (사용자좌표계 기준 거리) ───────────────────────── wooden_wheel_pallet_max_z_pos = 864 iron_wheel_pallet_max_z_pos = 884 -wooden_Flat_pallet_max_z_pos = 1192 # 227 - +wooden_Flat_pallet_max_z_pos = 1192 # 기준 팔레트 높이 227mm 일 때 값. /update_pallet 에서 보정됨 +# ── Modbus RTU (스태커 실린더 제어) 설정 ───────────────────────────────── port = 'COM3' baudrate = 115200 timeout = 2 - -# 모드버스 디바이스 설정 -slave_id = 1 # Slave ID는 1 -# Function code: 0x02 (Read Discrete Inputs) -start_address = 0x0000 # 시작 주소 -num_points = 8 # 읽을 포인트 수 (8개) -num_coils = 8 # 쓰기 할 코일의 개수 -instrument = minimalmodbus.Instrument(port, slave_id) # 포트와 슬레이브 ID 지정 -instrument.serial.baudrate = baudrate # 보드레이트 설정 -instrument.serial.timeout = timeout # 타임아웃 설정 -instrument.mode = minimalmodbus.MODE_RTU # RTU 모드 설정 -instrument.close_port_after_each_call = True # 포트를 자동으로 닫음 +slave_id = 1 # Modbus 슬레이브 ID +start_address = 0x0000 # 코일 시작 주소 +num_coils = 8 # 코일 개수 +instrument = minimalmodbus.Instrument(port, slave_id) +instrument.serial.baudrate = baudrate +instrument.serial.timeout = timeout +instrument.mode = minimalmodbus.MODE_RTU +instrument.close_port_after_each_call = True # 매 호출 후 COM 포트 자동 반환 (타 프로그램과 공유 가능) def Write_All_Do(coils_values): - global instrument - coils_values = coils_values - # 최소한의 모드버스 연결 객체 생성 + """스태커 Modbus 코일 8개를 한 번에 쓴다 (Function Code 15). + + coils_values: 길이 8 리스트. [0]=수직 실린더, [1]=수평 실린더, 나머지 예비. + """ try: - # Function Code 15 - 여러 코일 값 쓰기 instrument.write_bits(start_address, coils_values) print(f"Successfully wrote {num_coils} coils to slave {slave_id}") except IOError as e: print(f"Error writing coils: {e}") -Write_All_Do([0,0,0,0,0,0,0,0]) +Write_All_Do([0, 0, 0, 0, 0, 0, 0, 0]) # 앱 시작 시 스태커 실린더 전체 OFF (안전 초기화) + + def write_log(message): + """운영 로그 파일(C:/palletizing_log/log_YYYYMMDD.txt)에 한 줄 기록한다.""" try: - #폴더 없으면 생성 + # 폴더 없으면 생성 if not os.path.exists(log_folder_path): os.makedirs(log_folder_path) @@ -149,6 +217,7 @@ def write_log(message): def delete_old_logs(days): + """days 일보다 오래된 로그 파일을 삭제한다.""" try: #현재 날짜 - days 계산 deletion_date = datetime.now() - timedelta(days=days) @@ -171,8 +240,11 @@ def delete_old_logs(days): print(f"Error deleting logs: {e}") +# ═════════════════════ HTTP 라우트 (웹 UI ↔ 백엔드) ═════════════════════ + @api_routes.route('/writelog', methods=["POST"]) def writelog(): + """프론트엔드가 보낸 메시지를 운영 로그 파일에 기록한다.""" try: #적을 내용 message = request.get_data(as_text=True) @@ -191,6 +263,11 @@ def writelog(): @api_routes.route('/update_pallet', methods=['GET', 'POST']) def update_pallet(): + """UI에서 입력한 팔레트 규격(폭/길이/높이)을 반영한다. + + - 납작 팔레트 최대 하강값(wooden_Flat_pallet_max_z_pos)을 높이에 맞게 보정 + - 팔레트 원점 기준 사용자좌표계(6번)를 재설정 + """ global pallet_width, pallet_length, pallet_height, wooden_Flat_pallet_max_z_pos data = request.json @@ -218,9 +295,17 @@ def update_pallet(): return 'Success' -global selectedPalletId +global selectedPalletId # 선택된 팔레트 종류 문자열 (robot_start 에서 설정) + + @api_routes.route('/robot_start', methods=['POST']) def robot_start(): + """시작 버튼 처리: 팔레트 종류를 판별해 해당 적재 함수를 실행한다. + + - normalButton : 적재 메인 루프가 이 요청 안에서 그대로 돌기 때문에 + 작업이 끝날 때까지 HTTP 응답이 반환되지 않는다. + - oneLineButton: 책 좌표만 저장하고, 실제 실행은 /one_line_run 에서 한다. + """ global robot_start_clicked global book_center_positions global terminate_robot_button_clicked @@ -271,6 +356,7 @@ def robot_start(): @api_routes.route('/robot_finish_action', methods=['POST']) def robot_finish_action(): + """UI '로봇 동작 종료' 버튼: 메인 루프에 종료를 알리고 한줄쌓기 상태를 초기화한다.""" global action req_data = request.json @@ -295,6 +381,7 @@ def robot_finish_action(): @api_routes.route('/robot_immediately_take_book', methods=['POST']) def robot_immediately_take_book(): + """UI '즉시 픽업' 버튼: 책 준비 센서를 기다리지 않고 다음 사이클에 바로 픽업시킨다.""" global immediately_take_book req_data = request.json @@ -309,6 +396,7 @@ def robot_immediately_take_book(): @api_routes.route('/reset_data', methods=['POST']) def reset_data(): + """UI '리셋' 버튼: 일반/한줄쌓기 적재 진행 상태(json)를 모두 초기화한다.""" data = request.json if data.get('message') == 'Reset button clicked': @@ -337,6 +425,11 @@ def reset_data(): @api_routes.route('/one_line_run', methods=['POST']) def one_line_run(): + """한줄쌓기 시작: 선택 그리드(grid_id)에 대해 팔레트 종류별 한줄쌓기 루프 실행. + + 사전에 /update_pallet, /robot_start(oneLineButton)로 규격·좌표가 설정되어 있어야 한다. + normalButton 과 마찬가지로 작업 종료까지 HTTP 응답이 블록된다. + """ global grid_id global terminate_robot_button_clicked global action @@ -369,6 +462,7 @@ def one_line_run(): @api_routes.route('/one_line_stop', methods=['POST']) def one_line_stop(): + """UI '한줄쌓기 멈춤' 버튼: 현재 사이클 완료 후 루프를 종료시킨다.""" global is_one_line_stop_clicked data = request.json @@ -382,6 +476,7 @@ def one_line_stop(): @api_routes.route('/stacker_start', methods=['POST']) def stacker_start(): + """스태커 수동 시작 (이미 동작 중이면 무시).""" data = request.json if data.get('action') == 'stacker_start': @@ -401,6 +496,7 @@ def stacker_start(): @api_routes.route('/stacker_stop', methods=['POST']) def stacker_stop(): + """스태커 수동 정지.""" global stacker_thread_running data = request.json @@ -419,6 +515,7 @@ def stacker_stop(): @api_routes.route('/updown_on', methods=['POST']) def updown_on(): + """스태커 수직 실린더 수동 ON (스태커 자동 동작 중에는 거부).""" global stacker_thread_running data = request.json @@ -435,6 +532,7 @@ def updown_on(): @api_routes.route('/updown_off', methods=['POST']) def updown_off(): + """스태커 수직 실린더 수동 OFF (스태커 자동 동작 중에는 거부).""" global stacker_thread_running data = request.json @@ -451,6 +549,7 @@ def updown_off(): @api_routes.route('/leftright_on', methods=['POST']) def leftright_on(): + """스태커 수평 실린더 수동 ON (스태커 자동 동작 중에는 거부).""" global stacker_thread_running data = request.json @@ -467,6 +566,7 @@ def leftright_on(): @api_routes.route('/leftright_off', methods=['POST']) def leftright_off(): + """스태커 수평 실린더 수동 OFF (스태커 자동 동작 중에는 거부).""" global stacker_thread_running data = request.json @@ -481,6 +581,11 @@ def leftright_off(): return jsonify({'error': 'Invalid message'}), 400 def change_user_frame_coordiante(new_coord_data): + """사용자좌표계 6번('ScreenFlask')을 새 팔레트 기준 원점으로 설정한다. + + 모든 적재 모션(linear_move 등)이 이 좌표계 기준으로 동작하므로, + 팔레트 규격이 바뀌면 /update_pallet 를 통해 반드시 갱신되어야 한다. + """ robot.login() robot.power_on() robot.enable_robot() @@ -488,8 +593,18 @@ def change_user_frame_coordiante(new_coord_data): write_log("BE: change_user_frame_coordiante function successfully completed") return 'Success' -# 나무 바퀴 팔레트 일 때 (즉, ㅁ 모양으로 놓을때) def wooden_wheel_pallet(): + """[일반 적재] 나무 바퀴 팔레트: 층당 4권을 ㅁ자(井형) 배치로 적재. + + 책 1권 = 1사이클 처리 흐름: + 대기 자세 → 스태커 '책 준비' 센서 대기 → 픽업(진입-파지-리프트) + → 팔레트 진입 자세 → 그리드 상공 이동 → 하강 + 높이 탐색 → 릴리즈 + → 홈 복귀 → 진행 상태(cache.json) 저장 + - 홀수층/짝수층은 그리퍼 방향을 반대로 하여 맞물리게 쌓는다. + - 최대 적재 높이 도달 또는 UI '동작 종료' 시 루프를 빠져나간다. + - 비상정지 시 EMO()가 True를 반환하며 사이클을 중단하고, 해제되면 + get_status_thread 가 홈 복귀시킨 뒤 대기 자세부터 다시 진행한다. + """ write_log("BE: wooden_wheel_pallet function started") global action, immediately_take_book global emergency_stop, protected_stop_stat @@ -544,10 +659,14 @@ def wooden_wheel_pallet(): current_layer_height = 0 while True: + # busy-wait 방지: 짧게 쉬어 상태 폴링 스레드(GIL)가 센서값을 제때 갱신하도록 함 + time.sleep(IDLE_LOOP_INTERVAL) + if (EMO(0, 0)): is_move_pose = True continue + # UI '동작 종료' → 진행 상태 초기화 후 루프 종료 (스태커/컨베이어 정지) if action == 'robot_finish_action': action = None current_data_index = 0 @@ -571,6 +690,7 @@ def wooden_wheel_pallet(): write_log("BE: action == 'robot_finish_action' successfully completed - wooden_wheel_pallet") break + # 다음 층(130mm)을 쌓으면 최대 높이 초과 → 팔레트 완료 처리 후 종료 if current_layer_height + 130 > wheel_max_current_layer_height and current_data_index == 4: action = None current_data_index = 0 @@ -605,6 +725,8 @@ def wooden_wheel_pallet(): with open(cache_json_file_path, 'w', encoding='utf-8') as file: json.dump(cache_data, file, indent=4, ensure_ascii=False) + # ── 픽업 대기 자세 잡기 (사이클 완료/비상 복구 후 1회씩 수행) ── + # 홈 → 픽업 준비 자세 → 픽업 대기 높이 하강 순서로 이동해 두고 책을 기다린다. if is_move_pose == True and is_emo_disabled == True: is_move_pose = False if current_layer_index % 2 == 0: @@ -620,7 +742,7 @@ def wooden_wheel_pallet(): if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,666.816,0,0,0], 1,False, 8000, 3000, 0.1) # 공중에서 z축 내리기(-247.633) + robot.linear_move_extend([0, 0, 666.816, 0, 0, 0], 1, False, 8000, 3000, 0.1) # 픽업 대기 높이까지 하강(스태커 옆 공중 대기) write_log("BE: linear_move_extend : [0,0,666.816,0,0,0] (ready pose) successfully completed - wooden_wheel_pallet") is_in_pose() if (EMO(0, 0)): @@ -638,16 +760,19 @@ def wooden_wheel_pallet(): if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,681.817,0,0,0], 1, False, 8000, 3000, 0.1) # (-273.633) -253.633 --> -249.633 + robot.linear_move_extend([0, 0, 681.817, 0, 0, 0], 1, False, 8000, 3000, 0.1) # 픽업 대기 높이까지 하강(짝수층 대기 위치) write_log("BE: linear_move_extend : [0,0,681.817,0,0,0] (ready pose) successfully completed - wooden_wheel_pallet") is_in_pose() if (EMO(0, 0)): continue + # ── 픽업 시작 조건 ── + # (책 묶음 준비 AND 스태커 수직 실린더 완전 하강 AND 로봇 정상) + # 또는 (UI '즉시 픽업' AND 수직 실린더 상승 아님 AND 로봇 정상) if (((stacker_book_ready_sensor == 1) and (stacker_vertical_cyl_sensor == 0) and (stacker_vertical_cyl_down_sensor == 1)) and (emergency_stop == 0 or protected_stop_stat == 0) and (power_on_stat == 1 and enabled_stat == 1)) or (((immediately_take_book == 'robot_immediately_take_book') and (stacker_vertical_cyl_sensor == 0)) and (emergency_stop == 0 or protected_stop_stat == 0) and (power_on_stat == 1 and enabled_stat == 1)): immediately_take_book = None - if current_layer_index % 2 == 0: # 홀수 번째 레이어 + if current_layer_index % 2 == 0: # 홀수 번째 층(1,3,5...): 기본 방향으로 픽업 write_log("BE: load book on pallet's if condition started - wooden_wheel_pallet") current_data_list = list(book_center_positions) current_data = current_data_list[current_data_index] @@ -655,35 +780,36 @@ def wooden_wheel_pallet(): centerX = current_data['x'] centerY = current_data['y'] + # ── [픽업 시퀀스 시작] 그리퍼 2개(수평/수직) OFF 재확인 → 후퇴 대기 → 스태커 진입 ── robot.set_digital_output(IO_CABINET, 1, 0) write_log("BE: set_digital_output(IO_CABINET, 1, 0) (for safety purposes : parallel gripper cylinder off) successfully completed - wooden_wheel_pallet") robot.set_digital_output(IO_CABINET, 0, 0) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (for safety purposes : vertical gripper cylinder off) successfully completed - wooden_wheel_pallet") - time.sleep(save_time) + time.sleep(GRIPPER_RETRACT_WAIT) # (기존 save_time=2.0s) 실린더 후퇴 확인용 대기 - robot.linear_move_extend([-298.306, 0, 0,0,0,0], 1, False, 8000, 3000, 0.1) # x축 -해서 책 집을 위치로 가기 (-67.025) + robot.linear_move_extend([-298.306, 0, 0, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [픽업] 스태커로 수평 진입 (포크가 책 밑으로 들어감) write_log("BE: linear_move_extend : [-298.306, 0, 0,0,0,0] (move toward stacker to grip book) successfully completed - wooden_wheel_pallet") is_in_pose() if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 0, 1) # 그리퍼 수직 작동 + robot.set_digital_output(IO_CABINET, 0, 1) # [픽업] 수직 그리퍼 ON → 책 파지 write_log("BE: set_digital_output(IO_CABINET, 0, 1) (turn ON vertical gripper cylinder to grip book) successfully completed - wooden_wheel_pallet") - time.sleep(0.3) + time.sleep(GRIP_CLAMP_WAIT_ODD) # 파지(클램프) 완료 대기 if (EMO(0, 0)): continue - robot.linear_move_extend([0, 0, -694.816,0,0,0], 1, False, speed, acc, 0.1) # 책 떠서 올림 + robot.linear_move_extend([0, 0, -694.816, 0, 0, 0], 1, False, speed, acc, 0.1) # [픽업] 책을 뜬 채 팔레트 상공 높이까지 상승 write_log("BE: linear_move_extend : [0, 0, -694.816,0,0,0] (lift with book placed on gripper) successfully completed - wooden_wheel_pallet") is_in_pose() if (EMO(0, 1)): continue - time.sleep(0.3) + time.sleep(BOOK_SETTLE_WAIT) # 책 흔들림 안정화 if centerY > pallet_length / 2: - # 아래 팔레트 집입 자세 + # 아래 팔레트 진입 자세 robot.joint_move_extend(lower_pallet_enter_pose, 0, False, 8000, 3000, 0.1) write_log("BE: joint_move_extend : lower_pallet_enter_pose for odd layer grid #2 and #4 successfully completed - wooden_wheel_pallet") is_in_pose() @@ -721,7 +847,7 @@ def wooden_wheel_pallet(): get_distance_to_base(wooden_wheel_pallet_max_z_pos) else: - # 위에 팔레트 집입 자세 + # 위에 팔레트 진입 자세 robot.joint_move_extend(lower_pallet_enter_pose1, 0, False, 8000, 3000, 0.1) write_log("BE: joint_move_extend : lower_pallet_enter_pose1 for grid 1 and 3 successfully completed - wooden_wheel_pallet") is_in_pose() @@ -760,26 +886,27 @@ def wooden_wheel_pallet(): current_layer_height = z_pos - robot.set_digital_output(IO_CABINET, 0, 0) # 그리퍼 수직 off + # ── [적재 릴리즈] 수직 그리퍼 해제 → 푸셔 고정 → 포크 인출 → 푸셔 해제 ── + robot.set_digital_output(IO_CABINET, 0, 0) # [적재] 수직 그리퍼 OFF (책을 적재면에 내려놓음) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (gripper vertical cylinder off after put on pallet) successfully completed - wooden_wheel_pallet") - time.sleep(0.3) + time.sleep(RELEASE_WAIT) # (기존 0.3/0.2) 수직 그리퍼 후퇴 대기 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 1) # 그리퍼 수평 on + robot.set_digital_output(IO_CABINET, 1, 1) # [적재] 수평 푸셔 전진: 포크 인출 시 책이 딸려 나오지 않게 고정 write_log("BE: set_digital_output(IO_CABINET, 1, 1) (gripper parallel cylinder on after put on pallet) successfully completed - wooden_wheel_pallet") - time.sleep(1) + time.sleep(PUSH_HOLD_WAIT) # (기존 1.0s) 푸셔 스트로크 완료 대기 if (EMO(1, 1)): continue - robot.linear_move_extend([0, 0, -180, 0, 0, 0], 1, False, 8000, 3000, 0.1) + robot.linear_move_extend([0, 0, -180, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [적재] 포크만 위로 -180mm 인출 write_log(f"BE: linear_move_extend : [0, 0, -180, 0, 0, 0] (move robot little bit up) successfully completed - wooden_wheel_pallet") is_in_pose() - time.sleep(0.5) + time.sleep(FORK_EXIT_WAIT) # (기존 0.5s) 책 안정화 후 푸셔 해제 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 0) # 그리퍼 수평 off + robot.set_digital_output(IO_CABINET, 1, 0) # [적재] 수평 푸셔 OFF → 홈 복귀 후 다음 사이클 write_log("BE: set_digital_output(IO_CABINET, 1, 0) (after lift up turn OFF gripper parallel cylinder) successfully completed - wooden_wheel_pallet") if (EMO(1, 0)): continue @@ -800,7 +927,7 @@ def wooden_wheel_pallet(): - else: # 짝수 번째 레이어 (그리퍼 돌려서 책 들기) + else: # 짝수 번째 층(2,4,6...): 그리퍼를 돌려 반대 방향 픽업(맞물림 적재로 하중 분산) write_log("BE: load book on pallet's else condition started - wooden_wheel_pallet") current_data_list = list(book_center_positions) current_data = current_data_list[current_data_index] @@ -808,35 +935,36 @@ def wooden_wheel_pallet(): centerX = current_data['x'] centerY = current_data['y'] + # ── [픽업 시퀀스 시작] 그리퍼 2개(수평/수직) OFF 재확인 → 후퇴 대기 → 스태커 진입 ── robot.set_digital_output(IO_CABINET, 1, 0) write_log("BE: set_digital_output(IO_CABINET, 1, 0) (for safety purposes : parallel gripper cylinder off) successfully completed - wooden_wheel_pallet") robot.set_digital_output(IO_CABINET, 0, 0) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (for safety purposes : vertical gripper cylinder off) successfully completed - wooden_wheel_pallet") - time.sleep(save_time) + time.sleep(GRIPPER_RETRACT_WAIT) # (기존 save_time=2.0s) 실린더 후퇴 확인용 대기 - robot.linear_move_extend([194.344,0,0,0,0,0], 1, False, 8000, 3000, 0.1) # (-386.213) --> -387.213으로 하기 -->-384.208 + robot.linear_move_extend([194.344, 0, 0, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [픽업] 스태커로 수평 진입 (짝수층: 반대 방향에서 진입) write_log("BE: linear_move_extend : [194.344,0,0,0,0,0] (move toward stacker to grip book) successfully completed - wooden_wheel_pallet") is_in_pose() if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 0, 1) # 그리퍼 수직 on + robot.set_digital_output(IO_CABINET, 0, 1) # [픽업] 수직 그리퍼 ON → 책 파지 write_log("BE: set_digital_output(IO_CABINET, 0, 1) (turn ON vertical gripper cylinder to grip book) successfully completed - wooden_wheel_pallet") - time.sleep(0.1) + time.sleep(GRIP_CLAMP_WAIT_EVEN) # 파지(클램프) 완료 대기 if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,-703.816,0,0,0], 1, False, speed, acc, 0.1) # (-927.623) + robot.linear_move_extend([0, 0, -703.816, 0, 0, 0], 1, False, speed, acc, 0.1) # [픽업] 책을 뜬 채 팔레트 상공 높이까지 상승 write_log("BE: linear_move_extend : [0,0,-703.816,0,0,0] (lift with book placed on gripper) successfully completed - wooden_wheel_pallet") is_in_pose() if (EMO(0, 1)): continue - time.sleep(0.3) + time.sleep(BOOK_SETTLE_WAIT) # 책 흔들림 안정화 if centerY > pallet_length /2: - robot.joint_move_extend(lower_pallet_enter_pose, 0, False, 5, 1, 0.1) # 만약 책이 미끄러진다면 속도 조절 둘다 + robot.joint_move_extend(lower_pallet_enter_pose, 0, False, 5, 1, 0.1) # 짝수층은 파지 자세 특성상 저속 진입(책 미끄러짐 방지). 미끄러지면 speed/acc를 더 낮출 것 write_log("BE: joint_move_extend : lower_pallet_enter_pose for even layer grid 2 and 4 successfully completed - wooden_wheel_pallet") is_in_pose() if (EMO(0, 1)): @@ -911,26 +1039,27 @@ def wooden_wheel_pallet(): current_layer_height = z_pos - robot.set_digital_output(IO_CABINET, 0, 0) # 그리퍼 수직 off + # ── [적재 릴리즈] 수직 그리퍼 해제 → 푸셔 고정 → 포크 인출 → 푸셔 해제 ── + robot.set_digital_output(IO_CABINET, 0, 0) # [적재] 수직 그리퍼 OFF (책을 적재면에 내려놓음) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (gripper vertical cylinder off after put on pallet) successfully completed - wooden_wheel_pallet") - time.sleep(0.2) + time.sleep(RELEASE_WAIT) # (기존 0.3/0.2) 수직 그리퍼 후퇴 대기 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 1) # 그리퍼 수평 on + robot.set_digital_output(IO_CABINET, 1, 1) # [적재] 수평 푸셔 전진: 포크 인출 시 책이 딸려 나오지 않게 고정 write_log("BE: set_digital_output(IO_CABINET, 1, 1) (gripper parallel cylinder on after put on pallet) successfully completed - wooden_wheel_pallet") - time.sleep(1) + time.sleep(PUSH_HOLD_WAIT) # (기존 1.0s) 푸셔 스트로크 완료 대기 if (EMO(1, 1)): continue - robot.linear_move_extend([0, 0, -180, 0,0, 0], 1, False, 8000, 3000, 0.1) + robot.linear_move_extend([0, 0, -180, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [적재] 포크만 위로 -180mm 인출 write_log(f"BE: linear_move_extend : [0, 0, -180, 0, 0, 0] (move robot little bit up) successfully completed - wooden_wheel_pallet") is_in_pose() - time.sleep(0.5) + time.sleep(FORK_EXIT_WAIT) # (기존 0.5s) 책 안정화 후 푸셔 해제 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 0) # 그리퍼 수평 off + robot.set_digital_output(IO_CABINET, 1, 0) # [적재] 수평 푸셔 OFF → 홈 복귀 후 다음 사이클 write_log("BE: set_digital_output(IO_CABINET, 1, 0) (after lift up turn OFF gripper parallel cylinder) successfully completed - wooden_wheel_pallet") if (EMO(1, 0)): continue @@ -951,8 +1080,13 @@ def wooden_wheel_pallet(): -# 철제 바퀴 팔레트 일 때 (즉, ㅁ 모양으로 놓을때) def iron_wheel_pallet(): + """[일반 적재] 철제 바퀴 팔레트: 층당 4권 ㅁ자 배치. + + wooden_wheel_pallet() 과 동일한 시퀀스. 차이점: + - 최대 하강 z: iron_wheel_pallet_max_z_pos(884) + - 그리드 진입 자세/적재 좌표 오프셋이 철제 팔레트 실측 기준 + """ write_log("BE: iron_wheel_pallet function started") global action, immediately_take_book global emergency_stop, protected_stop_stat @@ -1011,10 +1145,14 @@ def iron_wheel_pallet(): current_layer_height = 0 while True: + # busy-wait 방지: 짧게 쉬어 상태 폴링 스레드(GIL)가 센서값을 제때 갱신하도록 함 + time.sleep(IDLE_LOOP_INTERVAL) + if (EMO(0, 0)): is_move_pose = True continue + # UI '동작 종료' → 진행 상태 초기화 후 루프 종료 (스태커/컨베이어 정지) if action == 'robot_finish_action': action = None current_data_index = 0 @@ -1038,6 +1176,7 @@ def iron_wheel_pallet(): write_log("BE: action == 'robot_finish_action' successfully completed - iron_wheel_pallet") break + # 다음 층(130mm)을 쌓으면 최대 높이 초과 → 팔레트 완료 처리 후 종료 if current_layer_height + 130 > wheel_max_current_layer_height and current_data_index == 4: action = None current_data_index = 0 @@ -1072,6 +1211,8 @@ def iron_wheel_pallet(): with open(cache_json_file_path, 'w', encoding='utf-8') as file: json.dump(cache_data, file, indent=4, ensure_ascii=False) + # ── 픽업 대기 자세 잡기 (사이클 완료/비상 복구 후 1회씩 수행) ── + # 홈 → 픽업 준비 자세 → 픽업 대기 높이 하강 순서로 이동해 두고 책을 기다린다. if is_move_pose == True and is_emo_disabled == True: is_move_pose = False if current_layer_index % 2 == 0: @@ -1087,7 +1228,7 @@ def iron_wheel_pallet(): if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,666.816,0,0,0], 1,False, 8000, 3000, 0.1) # 공중에서 z축 내리기(-247.633) + robot.linear_move_extend([0, 0, 666.816, 0, 0, 0], 1, False, 8000, 3000, 0.1) # 픽업 대기 높이까지 하강(스태커 옆 공중 대기) write_log("BE: linear_move_extend : [0,0,666.816,0,0,0] (ready pose) successfully completed - iron_wheel_pallet") is_in_pose() if (EMO(0, 0)): @@ -1105,16 +1246,19 @@ def iron_wheel_pallet(): if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,681.817,0,0,0], 1, False, 8000, 3000, 0.1) # (-273.633) -253.633 --> -249.633 + robot.linear_move_extend([0, 0, 681.817, 0, 0, 0], 1, False, 8000, 3000, 0.1) # 픽업 대기 높이까지 하강(짝수층 대기 위치) write_log("BE: linear_move_extend : [0,0,681.817,0,0,0] (ready pose) successfully completed - iron_wheel_pallet") is_in_pose() if (EMO(0, 0)): continue + # ── 픽업 시작 조건 ── + # (책 묶음 준비 AND 스태커 수직 실린더 완전 하강 AND 로봇 정상) + # 또는 (UI '즉시 픽업' AND 수직 실린더 상승 아님 AND 로봇 정상) if (((stacker_book_ready_sensor == 1) and (stacker_vertical_cyl_sensor == 0) and (stacker_vertical_cyl_down_sensor == 1)) and (emergency_stop == 0 or protected_stop_stat == 0) and (power_on_stat == 1 and enabled_stat == 1)) or (((immediately_take_book == 'robot_immediately_take_book') and (stacker_vertical_cyl_sensor == 0)) and (emergency_stop == 0 or protected_stop_stat == 0) and (power_on_stat == 1 and enabled_stat == 1)): immediately_take_book = None - if current_layer_index % 2 == 0: # 홀수 번째 레이어 + if current_layer_index % 2 == 0: # 홀수 번째 층(1,3,5...): 기본 방향으로 픽업 write_log("BE: load book on pallet's if condition started - iron_wheel_pallet") current_data_list = list(book_center_positions) current_data = current_data_list[current_data_index] @@ -1122,35 +1266,36 @@ def iron_wheel_pallet(): centerX = current_data['x'] centerY = current_data['y'] + # ── [픽업 시퀀스 시작] 그리퍼 2개(수평/수직) OFF 재확인 → 후퇴 대기 → 스태커 진입 ── robot.set_digital_output(IO_CABINET, 1, 0) write_log("BE: set_digital_output(IO_CABINET, 1, 0) (for safety purposes : parallel gripper cylinder off) successfully completed - iron_wheel_pallet") robot.set_digital_output(IO_CABINET, 0, 0) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (for safety purposes : vertical gripper cylinder off) successfully completed - iron_wheel_pallet") - time.sleep(save_time) + time.sleep(GRIPPER_RETRACT_WAIT) # (기존 save_time=2.0s) 실린더 후퇴 확인용 대기 - robot.linear_move_extend([-298.306, 0, 0,0,0,0], 1, False, 8000, 3000, 0.1) # x축 -해서 책 집을 위치로 가기 (-67.025) + robot.linear_move_extend([-298.306, 0, 0, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [픽업] 스태커로 수평 진입 (포크가 책 밑으로 들어감) write_log("BE: linear_move_extend : [-298.306, 0, 0,0,0,0] (move toward stacker to grip book) successfully completed - iron_wheel_pallet") is_in_pose() if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 0, 1) # 그리퍼 수직 작동 + robot.set_digital_output(IO_CABINET, 0, 1) # [픽업] 수직 그리퍼 ON → 책 파지 write_log("BE: set_digital_output(IO_CABINET, 0, 1) (turn ON vertical gripper cylinder to grip book) successfully completed - iron_wheel_pallet") - time.sleep(0.3) + time.sleep(GRIP_CLAMP_WAIT_ODD) # 파지(클램프) 완료 대기 if (EMO(0, 0)): continue - robot.linear_move_extend([0, 0, -694.816,0,0,0], 1, False, speed, acc, 0.1) # 책 떠서 올림 + robot.linear_move_extend([0, 0, -694.816, 0, 0, 0], 1, False, speed, acc, 0.1) # [픽업] 책을 뜬 채 팔레트 상공 높이까지 상승 write_log("BE: linear_move_extend : [0, 0, -694.816,0,0,0] (lift with book placed on gripper) successfully completed - iron_wheel_pallet") is_in_pose() if (EMO(0, 1)): continue - time.sleep(0.3) + time.sleep(BOOK_SETTLE_WAIT) # 책 흔들림 안정화 if centerY > pallet_length / 2: - # 아래 팔레트 집입 자세 + # 아래 팔레트 진입 자세 robot.joint_move_extend(lower_pallet_enter_pose, 0, False, 8000, 3000, 0.1) write_log("BE: joint_move_extend : lower_pallet_enter_pose for odd layer grid 2 and 4 successfully completed - iron_wheel_pallet") is_in_pose() @@ -1188,7 +1333,7 @@ def iron_wheel_pallet(): get_distance_to_base(iron_wheel_pallet_max_z_pos) else: - # 위에 팔레트 집입 자세 + # 위에 팔레트 진입 자세 robot.joint_move_extend(lower_pallet_enter_pose1, 0, False, 8000, 3000, 0.1) write_log("BE: joint_move_extend : lower_pallet_enter_pose1 for odd layer grid 1 and 3 successfully completed - iron_wheel_pallet") is_in_pose() @@ -1227,26 +1372,27 @@ def iron_wheel_pallet(): current_layer_height = z_pos - robot.set_digital_output(IO_CABINET, 0, 0) # 그리퍼 수직 off + # ── [적재 릴리즈] 수직 그리퍼 해제 → 푸셔 고정 → 포크 인출 → 푸셔 해제 ── + robot.set_digital_output(IO_CABINET, 0, 0) # [적재] 수직 그리퍼 OFF (책을 적재면에 내려놓음) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (gripper vertical cylinder off after put on pallet) successfully completed - iron_wheel_pallet") - time.sleep(0.3) + time.sleep(RELEASE_WAIT) # (기존 0.3/0.2) 수직 그리퍼 후퇴 대기 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 1) # 그리퍼 수평 on + robot.set_digital_output(IO_CABINET, 1, 1) # [적재] 수평 푸셔 전진: 포크 인출 시 책이 딸려 나오지 않게 고정 write_log("BE: set_digital_output(IO_CABINET, 1, 1) (gripper parallel cylinder on after put on pallet) successfully completed - iron_wheel_pallet") - time.sleep(1) + time.sleep(PUSH_HOLD_WAIT) # (기존 1.0s) 푸셔 스트로크 완료 대기 if (EMO(1, 1)): continue - robot.linear_move_extend([0,0, -180, 0,0, 0], 1, False, 8000, 3000, 0.1) + robot.linear_move_extend([0, 0, -180, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [적재] 포크만 위로 -180mm 인출 write_log(f"BE: linear_move_extend : [0, 0, -180, 0, 0, 0] (move robot little bit up) successfully completed - iron_wheel_pallet") is_in_pose() - time.sleep(0.5) + time.sleep(FORK_EXIT_WAIT) # (기존 0.5s) 책 안정화 후 푸셔 해제 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 0) # 그리퍼 수평 off + robot.set_digital_output(IO_CABINET, 1, 0) # [적재] 수평 푸셔 OFF → 홈 복귀 후 다음 사이클 write_log("BE: set_digital_output(IO_CABINET, 1, 0) (after lift up turn OFF gripper parallel cylinder) successfully completed - iron_wheel_pallet") if (EMO(1, 0)): continue @@ -1267,7 +1413,7 @@ def iron_wheel_pallet(): is_move_pose = True - else: # 짝수 번째 레이어 (그리퍼 돌려서 책 들기) + else: # 짝수 번째 층(2,4,6...): 그리퍼를 돌려 반대 방향 픽업(맞물림 적재로 하중 분산) write_log("BE: load book on pallet's else condition started - iron_wheel_pallet") current_data_list = list(book_center_positions) current_data = current_data_list[current_data_index] @@ -1275,35 +1421,36 @@ def iron_wheel_pallet(): centerX = current_data['x'] centerY = current_data['y'] + # ── [픽업 시퀀스 시작] 그리퍼 2개(수평/수직) OFF 재확인 → 후퇴 대기 → 스태커 진입 ── robot.set_digital_output(IO_CABINET, 1, 0) write_log("BE: set_digital_output(IO_CABINET, 1, 0) (for safety purposes : parallel gripper cylinder off) successfully completed - iron_wheel_pallet") robot.set_digital_output(IO_CABINET, 0, 0) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (for safety purposes : vertical gripper cylinder off) successfully completed - iron_wheel_pallet") - time.sleep(save_time) + time.sleep(GRIPPER_RETRACT_WAIT) # (기존 save_time=2.0s) 실린더 후퇴 확인용 대기 - robot.linear_move_extend([194.344,0,0,0,0,0], 1, False, 8000, 3000, 0.1) # (-386.213) --> -387.213으로 하기 -->-384.208 + robot.linear_move_extend([194.344, 0, 0, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [픽업] 스태커로 수평 진입 (짝수층: 반대 방향에서 진입) write_log("BE: linear_move_extend : [194.344,0,0,0,0,0] (move toward stacker to grip book) successfully completed - iron_wheel_pallet") is_in_pose() if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 0, 1) # 그리퍼 수직 on + robot.set_digital_output(IO_CABINET, 0, 1) # [픽업] 수직 그리퍼 ON → 책 파지 write_log("BE: set_digital_output(IO_CABINET, 0, 1) (turn ON vertical gripper cylinder to grip book) successfully completed - iron_wheel_pallet") - time.sleep(0.1) + time.sleep(GRIP_CLAMP_WAIT_EVEN) # 파지(클램프) 완료 대기 if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,-703.816,0,0,0], 1, False, speed, acc, 0.1) # (-927.623) + robot.linear_move_extend([0, 0, -703.816, 0, 0, 0], 1, False, speed, acc, 0.1) # [픽업] 책을 뜬 채 팔레트 상공 높이까지 상승 write_log("BE: linear_move_extend : [0,0,-703.816,0,0,0] (lift with book placed on gripper) successfully completed - iron_wheel_pallet") is_in_pose() if (EMO(0, 1)): continue - time.sleep(0.3) + time.sleep(BOOK_SETTLE_WAIT) # 책 흔들림 안정화 if centerY > pallet_length /2: - robot.joint_move_extend(lower_pallet_enter_pose, 0, False, 5, 1, 0.1) # 만약 책이 미끄러진다면 속도 조절 둘다 + robot.joint_move_extend(lower_pallet_enter_pose, 0, False, 5, 1, 0.1) # 짝수층은 파지 자세 특성상 저속 진입(책 미끄러짐 방지). 미끄러지면 speed/acc를 더 낮출 것 write_log("BE: joint_move_extend : lower_pallet_enter_pose for even layer gird 2 and 4 successfully completed - iron_wheel_pallet") is_in_pose() if (EMO(0, 1)): @@ -1380,26 +1527,27 @@ def iron_wheel_pallet(): # @@@@@@@@@@@@@@@@@@@current_layer_height 에 z값 저장 (z값은 is_in_pose 함수에서 가져옴, )@@@@@@@@@@@@@@@@@@@@@ current_layer_height = z_pos - robot.set_digital_output(IO_CABINET, 0, 0) # 그리퍼 수직 off + # ── [적재 릴리즈] 수직 그리퍼 해제 → 푸셔 고정 → 포크 인출 → 푸셔 해제 ── + robot.set_digital_output(IO_CABINET, 0, 0) # [적재] 수직 그리퍼 OFF (책을 적재면에 내려놓음) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (gripper vertical cylinder off after put on pallet) successfully completed - iron_wheel_pallet") - time.sleep(0.2) + time.sleep(RELEASE_WAIT) # (기존 0.3/0.2) 수직 그리퍼 후퇴 대기 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 1) # 그리퍼 수평 on + robot.set_digital_output(IO_CABINET, 1, 1) # [적재] 수평 푸셔 전진: 포크 인출 시 책이 딸려 나오지 않게 고정 write_log("BE: set_digital_output(IO_CABINET, 1, 1) (gripper parallel cylinder on after put on pallet) successfully completed - iron_wheel_pallet") - time.sleep(1) + time.sleep(PUSH_HOLD_WAIT) # (기존 1.0s) 푸셔 스트로크 완료 대기 if (EMO(1, 1)): continue - robot.linear_move_extend([0,0, -180, 0,0, 0], 1, False, 8000, 3000, 0.1) + robot.linear_move_extend([0, 0, -180, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [적재] 포크만 위로 -180mm 인출 write_log(f"BE: linear_move_extend : [0,0, -180, 0,0, 0] (move robot little bit up) successfully completed - iron_wheel_pallet") is_in_pose() - time.sleep(0.5) + time.sleep(FORK_EXIT_WAIT) # (기존 0.5s) 책 안정화 후 푸셔 해제 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 0) # 그리퍼 수평 off + robot.set_digital_output(IO_CABINET, 1, 0) # [적재] 수평 푸셔 OFF → 홈 복귀 후 다음 사이클 write_log("BE: set_digital_output(IO_CABINET, 1, 0) (after lift up turn OFF gripper parallel cylinder) successfully completed - iron_wheel_pallet") if (EMO(1, 0)): continue @@ -1420,8 +1568,14 @@ def iron_wheel_pallet(): is_move_pose = True -# 나무 납작 팔레트 일 때 def wooden_Flat_pallet(): + """[일반 적재] 나무 납작(낮은) 팔레트: 층당 8권을 그리퍼 -92.3° 회전 자세로 적재. + + wooden_wheel_pallet() 과 동일한 시퀀스. 차이점: + - 층당 8권(그리드 1~8), 적재 가능 높이가 큼(flat_max_current_layer_height=1080) + - 책 하중 보정을 위해 set_payload(14~15kg) 호출 + - 첫 1~2층은 저속(2500mm/s) 하강으로 안착 안정성 확보 + """ write_log("BE: wooden_Flat_pallet function started") global action, immediately_take_book global emergency_stop, protected_stop_stat @@ -1482,10 +1636,14 @@ def wooden_Flat_pallet(): while True: + # busy-wait 방지: 짧게 쉬어 상태 폴링 스레드(GIL)가 센서값을 제때 갱신하도록 함 + time.sleep(IDLE_LOOP_INTERVAL) + if (EMO(0, 0)): is_move_pose = True continue + # UI '동작 종료' → 진행 상태 초기화 후 루프 종료 (스태커/컨베이어 정지) if action == 'robot_finish_action': action = None current_data_index = 0 @@ -1509,6 +1667,7 @@ def wooden_Flat_pallet(): write_log("BE: action == 'robot_finish_action' successfully completed - wooden_Flat_pallet") break + # 다음 층(130mm)을 쌓으면 최대 높이 초과 → 팔레트 완료 처리 후 종료 if current_layer_height + 130 > flat_max_current_layer_height and current_data_index == 8: action = None current_data_index = 0 @@ -1543,6 +1702,8 @@ def wooden_Flat_pallet(): with open(cache_json_file_path, 'w', encoding='utf-8') as file: json.dump(cache_data, file, indent=4, ensure_ascii=False) + # ── 픽업 대기 자세 잡기 (사이클 완료/비상 복구 후 1회씩 수행) ── + # 홈 → 픽업 준비 자세 → 픽업 대기 높이 하강 순서로 이동해 두고 책을 기다린다. if is_move_pose == True and is_emo_disabled == True: is_move_pose = False if current_layer_index % 2 == 0: @@ -1558,7 +1719,7 @@ def wooden_Flat_pallet(): if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,666.816,0,0,0], 1,False, 8000, 3000, 0.1) # 공중에서 z축 내리기(-247.633) + robot.linear_move_extend([0, 0, 666.816, 0, 0, 0], 1, False, 8000, 3000, 0.1) # 픽업 대기 높이까지 하강(스태커 옆 공중 대기) write_log("BE: linear_move_extend : [0,0,666.816,0,0,0] (ready pose) successfully completed - wooden_Flat_pallet") is_in_pose() if (EMO(0, 0)): @@ -1576,16 +1737,19 @@ def wooden_Flat_pallet(): if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,681.817,0,0,0], 1, False, 8000, 3000, 0.1) # (-273.633) -253.633 --> -249.633 + robot.linear_move_extend([0, 0, 681.817, 0, 0, 0], 1, False, 8000, 3000, 0.1) # 픽업 대기 높이까지 하강(짝수층 대기 위치) write_log("BE: linear_move_extend : [0,0,681.817,0,0,0] (ready pose) successfully completed - wooden_Flat_pallet") is_in_pose() if (EMO(0, 0)): continue + # ── 픽업 시작 조건 ── + # (책 묶음 준비 AND 스태커 수직 실린더 완전 하강 AND 로봇 정상) + # 또는 (UI '즉시 픽업' AND 수직 실린더 상승 아님 AND 로봇 정상) if (((stacker_book_ready_sensor == 1) and (stacker_vertical_cyl_sensor == 0) and (stacker_vertical_cyl_down_sensor == 1)) and (emergency_stop == 0 or protected_stop_stat == 0) and (power_on_stat == 1 and enabled_stat == 1)) or (((immediately_take_book == 'robot_immediately_take_book') and (stacker_vertical_cyl_sensor == 0)) and (emergency_stop == 0 or protected_stop_stat == 0) and (power_on_stat == 1 and enabled_stat == 1)): immediately_take_book = None - if current_layer_index % 2 == 0: # 홀수 번째 레이어 + if current_layer_index % 2 == 0: # 홀수 번째 층(1,3,5...): 기본 방향으로 픽업 write_log("BE: load book on pallet's if condition started - wooden_Flat_pallet") current_data_list = list(book_center_positions) current_data = current_data_list[current_data_index] @@ -1593,32 +1757,33 @@ def wooden_Flat_pallet(): centerX = current_data['x'] centerY = current_data['y'] + # ── [픽업 시퀀스 시작] 그리퍼 2개(수평/수직) OFF 재확인 → 후퇴 대기 → 스태커 진입 ── robot.set_digital_output(IO_CABINET, 1, 0) write_log("BE: set_digital_output(IO_CABINET, 1, 0) (for safety purposes : parallel gripper cylinder off) successfully completed - wooden_Flat_pallet") robot.set_digital_output(IO_CABINET, 0, 0) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (for safety purposes : vertical gripper cylinder off) successfully completed - wooden_Flat_pallet") - time.sleep(save_time) + time.sleep(GRIPPER_RETRACT_WAIT) # (기존 save_time=2.0s) 실린더 후퇴 확인용 대기 - robot.linear_move_extend([-298.306, 0, 0,0,0,0], 1, False, 8000, 3000, 0.1) # x축 -해서 책 집을 위치로 가기 (-67.025) + robot.linear_move_extend([-298.306, 0, 0, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [픽업] 스태커로 수평 진입 (포크가 책 밑으로 들어감) write_log("BE: linear_move_extend : [-298.306, 0, 0,0,0,0] (move toward stacker to grip book) successfully completed - wooden_Flat_pallet") is_in_pose() if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 0, 1) # 그리퍼 수직 작동 + robot.set_digital_output(IO_CABINET, 0, 1) # [픽업] 수직 그리퍼 ON → 책 파지 write_log("BE: set_digital_output(IO_CABINET, 0, 1) (turn ON vertical gripper cylinder to grip book) successfully completed - wooden_Flat_pallet") - time.sleep(0.3) + time.sleep(GRIP_CLAMP_WAIT_ODD) # 파지(클램프) 완료 대기 if (EMO(0, 0)): continue - robot.linear_move_extend([0, 0, -694.816,0,0,0], 1, False, speed, acc, 0.1) # 책 떠서 올림 + robot.linear_move_extend([0, 0, -694.816, 0, 0, 0], 1, False, speed, acc, 0.1) # [픽업] 책을 뜬 채 팔레트 상공 높이까지 상승 write_log("BE: linear_move_extend : [0, 0, -694.816,0,0,0] (lift with book placed on gripper) successfully completed - wooden_Flat_pallet") is_in_pose() if (EMO(0, 1)): continue - time.sleep(0.3) + time.sleep(BOOK_SETTLE_WAIT) # 책 흔들림 안정화 if centerY < pallet_length / 2: # 4 3 2 1 @@ -1766,26 +1931,27 @@ def wooden_Flat_pallet(): robot.set_payload(mass= 14, centroid =[0,0,0]) write_log("Set payload 14") - robot.set_digital_output(IO_CABINET, 0, 0) # 그리퍼 수직 off + # ── [적재 릴리즈] 수직 그리퍼 해제 → 푸셔 고정 → 포크 인출 → 푸셔 해제 ── + robot.set_digital_output(IO_CABINET, 0, 0) # [적재] 수직 그리퍼 OFF (책을 적재면에 내려놓음) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (gripper vertical cylinder off after put on pallet) successfully completed - wooden_Flat_pallet") - time.sleep(0.3) + time.sleep(RELEASE_WAIT) # (기존 0.3/0.2) 수직 그리퍼 후퇴 대기 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 1) # 그리퍼 수평 on + robot.set_digital_output(IO_CABINET, 1, 1) # [적재] 수평 푸셔 전진: 포크 인출 시 책이 딸려 나오지 않게 고정 write_log("BE: set_digital_output(IO_CABINET, 1, 1) (gripper parallel cylinder on after put on pallet) successfully completed - wooden_Flat_pallet") - time.sleep(1) + time.sleep(PUSH_HOLD_WAIT) # (기존 1.0s) 푸셔 스트로크 완료 대기 if (EMO(1, 1)): continue - robot.linear_move_extend([0,0, -180, 0,0, 0], 1, False, 8000, 3000, 0.1) + robot.linear_move_extend([0, 0, -180, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [적재] 포크만 위로 -180mm 인출 write_log(f"BE: linear_move_extend : [0, 0, -180, 0, 0, 0] (move robot little bit up) successfully completed - wooden_Flat_pallet") is_in_pose() - time.sleep(0.5) + time.sleep(FORK_EXIT_WAIT) # (기존 0.5s) 책 안정화 후 푸셔 해제 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 0) # 그리퍼 수평 off + robot.set_digital_output(IO_CABINET, 1, 0) # [적재] 수평 푸셔 OFF → 홈 복귀 후 다음 사이클 write_log("BE: set_digital_output(IO_CABINET, 1, 0) (after lift up turn OFF gripper parallel cylinder) successfully completed - wooden_Flat_pallet") if (EMO(1, 0)): continue @@ -1810,7 +1976,7 @@ def wooden_Flat_pallet(): is_move_pose = True - else: # 짝수 번째 레이어 (그리퍼 돌려서 책 들기) + else: # 짝수 번째 층(2,4,6...): 그리퍼를 돌려 반대 방향 픽업(맞물림 적재로 하중 분산) write_log("BE: load book on pallet's else condition started - wooden_Flat_pallet") current_data_list = list(book_center_positions) current_data = current_data_list[current_data_index] @@ -1818,35 +1984,36 @@ def wooden_Flat_pallet(): centerX = current_data['x'] centerY = current_data['y'] + # ── [픽업 시퀀스 시작] 그리퍼 2개(수평/수직) OFF 재확인 → 후퇴 대기 → 스태커 진입 ── robot.set_digital_output(IO_CABINET, 1, 0) write_log("BE: set_digital_output(IO_CABINET, 1, 0) (for safety purposes : parallel gripper cylinder off) successfully completed - wooden_Flat_pallet") robot.set_digital_output(IO_CABINET, 0, 0) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (for safety purposes : vertical gripper cylinder off) successfully completed - wooden_Flat_pallet") - time.sleep(save_time) + time.sleep(GRIPPER_RETRACT_WAIT) # (기존 save_time=2.0s) 실린더 후퇴 확인용 대기 - robot.linear_move_extend([194.344,0,0,0,0,0], 1, False, 8000, 3000, 0.1) # (-386.213) --> -387.213으로 하기 -->-384.208 + robot.linear_move_extend([194.344, 0, 0, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [픽업] 스태커로 수평 진입 (짝수층: 반대 방향에서 진입) write_log("BE: linear_move_extend : [194.344,0,0,0,0,0] (move toward stacker to grip book) successfully completed - wooden_Flat_pallet") is_in_pose() if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 0, 1) # 그리퍼 수직 on + robot.set_digital_output(IO_CABINET, 0, 1) # [픽업] 수직 그리퍼 ON → 책 파지 write_log("BE: set_digital_output(IO_CABINET, 0, 1) (turn ON vertical gripper cylinder to grip book) successfully completed - wooden_Flat_pallet") - time.sleep(0.1) + time.sleep(GRIP_CLAMP_WAIT_EVEN) # 파지(클램프) 완료 대기 if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,-703.816,0,0,0], 1, False, speed, acc, 0.1) # (-927.623) + robot.linear_move_extend([0, 0, -703.816, 0, 0, 0], 1, False, speed, acc, 0.1) # [픽업] 책을 뜬 채 팔레트 상공 높이까지 상승 write_log("BE: linear_move_extend : [0,0,-703.816,0,0,0] (lift with book placed on gripper) successfully completed - wooden_Flat_pallet") is_in_pose() if (EMO(0, 1)): continue - time.sleep(0.3) + time.sleep(BOOK_SETTLE_WAIT) # 책 흔들림 안정화 if centerY < pallet_length /2: - robot.joint_move_extend(low_profile_1_to_4_enter_pose, 0, False, 8000, 3000, 0.1) # 만약 책이 미끄러진다면 속도 조절 둘다 + robot.joint_move_extend(low_profile_1_to_4_enter_pose, 0, False, 8000, 3000, 0.1) # 짝수층은 파지 자세 특성상 저속 진입(책 미끄러짐 방지). 미끄러지면 speed/acc를 더 낮출 것 write_log("BE: joint_move_extend : low_profile_1_to_4_enter_pose for even layer gird 1,2,3,4 successfully completed - wooden_Flat_pallet") is_in_pose() if (EMO(0, 1)): @@ -1992,26 +2159,27 @@ def wooden_Flat_pallet(): robot.set_payload(mass= 14, centroid =[0,0,0]) write_log("Set payload 14") - robot.set_digital_output(IO_CABINET, 0, 0) # 그리퍼 수직 off + # ── [적재 릴리즈] 수직 그리퍼 해제 → 푸셔 고정 → 포크 인출 → 푸셔 해제 ── + robot.set_digital_output(IO_CABINET, 0, 0) # [적재] 수직 그리퍼 OFF (책을 적재면에 내려놓음) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (gripper vertical cylinder off after put on pallet) successfully completed - wooden_Flat_pallet") - time.sleep(0.3) + time.sleep(RELEASE_WAIT) # (기존 0.3/0.2) 수직 그리퍼 후퇴 대기 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 1) # 그리퍼 수평 on + robot.set_digital_output(IO_CABINET, 1, 1) # [적재] 수평 푸셔 전진: 포크 인출 시 책이 딸려 나오지 않게 고정 write_log("BE: set_digital_output(IO_CABINET, 1, 1) (gripper parallel cylinder on after put on pallet) successfully completed - wooden_Flat_pallet") - time.sleep(1) + time.sleep(PUSH_HOLD_WAIT) # (기존 1.0s) 푸셔 스트로크 완료 대기 if (EMO(1, 1)): continue - robot.linear_move_extend([0, 0, -180, 0, 0, 0], 1, False, 8000, 3000, 0.1) + robot.linear_move_extend([0, 0, -180, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [적재] 포크만 위로 -180mm 인출 write_log(f"BE: linear_move_extend : [0,0, -180, 0, 0, 0] (move robot little bit up) successfully completed - wooden_Flat_pallet") is_in_pose() - time.sleep(0.5) + time.sleep(FORK_EXIT_WAIT) # (기존 0.5s) 책 안정화 후 푸셔 해제 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 0) # 그리퍼 수평 off + robot.set_digital_output(IO_CABINET, 1, 0) # [적재] 수평 푸셔 OFF → 홈 복귀 후 다음 사이클 write_log("BE: set_digital_output(IO_CABINET, 1, 0) (after lift up turn OFF gripper parallel cylinder) successfully completed - wooden_Flat_pallet") if (EMO(1, 0)): continue @@ -2035,8 +2203,14 @@ def wooden_Flat_pallet(): is_move_pose = True -# currentDataIndex는 고정값인 것 잊지 않기 def one_line_wooden_wheel_pallet(): + """[한줄쌓기] 나무 바퀴 팔레트: 선택한 그리드 한 곳에만 수직으로 계속 쌓는 모드. + + 일반 적재와 달리 UI에서 고른 grid_id 위치에만 적재한다. + - currentDataIndex 는 그리드 번호 고정값(0~3)이며 증가하지 않는다. + - 층수/높이 진행 상태는 oneLine.json 에 그리드별로 저장한다. + - 모든 그리드가 최대 높이에 도달하면 자동 종료된다. + """ write_log("BE: one_line_wooden_wheel_pallet function started") global action, immediately_take_book global emergency_stop, protected_stop_stat @@ -2094,11 +2268,15 @@ def one_line_wooden_wheel_pallet(): current_layer_height = 0 while True: + # busy-wait 방지: 짧게 쉬어 상태 폴링 스레드(GIL)가 센서값을 제때 갱신하도록 함 + time.sleep(IDLE_LOOP_INTERVAL) + if (EMO(0, 0)): is_move_pose = True continue # 로봇 동작 종료 누르면 초기화 및 while 탈출 / 스태커 멈춤 + # UI '동작 종료' → 진행 상태 초기화 후 루프 종료 (스태커/컨베이어 정지) if action == 'robot_finish_action': action = None current_layer_index = 0 @@ -2158,6 +2336,8 @@ def one_line_wooden_wheel_pallet(): write_log("BE: current_layer_height > oneline_wheel_max_current_layer_height successfully completed - one_line_wooden_wheel_pallet") break + # ── 픽업 대기 자세 잡기 (사이클 완료/비상 복구 후 1회씩 수행) ── + # 홈 → 픽업 준비 자세 → 픽업 대기 높이 하강 순서로 이동해 두고 책을 기다린다. if is_move_pose == True and is_emo_disabled == True: is_move_pose = False if current_layer_index % 2 == 0: @@ -2173,7 +2353,7 @@ def one_line_wooden_wheel_pallet(): if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,666.816,0,0,0], 1,False, 8000, 3000, 0.1) # 공중에서 z축 내리기(-247.633) + robot.linear_move_extend([0, 0, 666.816, 0, 0, 0], 1, False, 8000, 3000, 0.1) # 픽업 대기 높이까지 하강(스태커 옆 공중 대기) write_log("BE: linear_move_extend : [0,0,666.816,0,0,0] (reday pose) successfully completed - one_line_wooden_wheel_pallet") is_in_pose() if (EMO(0, 0)): @@ -2191,16 +2371,19 @@ def one_line_wooden_wheel_pallet(): if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,681.817,0,0,0], 1, False, 8000, 3000, 0.1) # (-273.633) -253.633 --> -249.633 + robot.linear_move_extend([0, 0, 681.817, 0, 0, 0], 1, False, 8000, 3000, 0.1) # 픽업 대기 높이까지 하강(짝수층 대기 위치) write_log("BE: linear_move_extend : [0,0,681.817,0,0,0] (ready pose) successfully completed - one_line_wooden_wheel_pallet") is_in_pose() if (EMO(0, 0)): continue + # ── 픽업 시작 조건 ── + # (책 묶음 준비 AND 스태커 수직 실린더 완전 하강 AND 로봇 정상) + # 또는 (UI '즉시 픽업' AND 수직 실린더 상승 아님 AND 로봇 정상) if (((stacker_book_ready_sensor == 1) and (stacker_vertical_cyl_sensor == 0) and (stacker_vertical_cyl_down_sensor == 1)) and (emergency_stop == 0 or protected_stop_stat == 0) and (power_on_stat == 1 and enabled_stat == 1)) or (((immediately_take_book == 'robot_immediately_take_book') and (stacker_vertical_cyl_sensor == 0)) and (emergency_stop == 0 or protected_stop_stat == 0) and (power_on_stat == 1 and enabled_stat == 1)): immediately_take_book = None - if current_layer_index % 2 == 0: # 홀수 번째 레이어 + if current_layer_index % 2 == 0: # 홀수 번째 층(1,3,5...): 기본 방향으로 픽업 write_log("BE: load book on pallet's if condition started - one_line_wooden_wheel_pallet") current_data_list = list(book_center_positions) current_data = current_data_list[current_data_index] @@ -2208,35 +2391,36 @@ def one_line_wooden_wheel_pallet(): centerX = current_data['x'] centerY = current_data['y'] + # ── [픽업 시퀀스 시작] 그리퍼 2개(수평/수직) OFF 재확인 → 후퇴 대기 → 스태커 진입 ── robot.set_digital_output(IO_CABINET, 1, 0) write_log("BE: set_digital_output(IO_CABINET, 1, 0) (for safety purposes : parallel gripper cylinder off) successfully completed - one_line_wooden_wheel_pallet") robot.set_digital_output(IO_CABINET, 0, 0) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (for safety purposes : vertical gripper cylinder off) successfully completed - one_line_wooden_wheel_pallet") - time.sleep(save_time) + time.sleep(GRIPPER_RETRACT_WAIT) # (기존 save_time=2.0s) 실린더 후퇴 확인용 대기 - robot.linear_move_extend([-298.306, 0, 0,0,0,0], 1, False, 8000, 3000, 0.1) # x축 -해서 책 집을 위치로 가기 (-67.025) + robot.linear_move_extend([-298.306, 0, 0, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [픽업] 스태커로 수평 진입 (포크가 책 밑으로 들어감) write_log("BE: linear_move_extend : [-298.306, 0, 0,0,0,0] (move toward stacker to grip book) successfully completed - one_line_wooden_wheel_pallet") is_in_pose() if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 0, 1) # 그리퍼 수직 작동 + robot.set_digital_output(IO_CABINET, 0, 1) # [픽업] 수직 그리퍼 ON → 책 파지 write_log("BE: set_digital_output(IO_CABINET, 0, 1) (turn ON vertical gripper cylinder to grip book) successfully completed - one_line_wooden_wheel_pallet") - time.sleep(0.3) + time.sleep(GRIP_CLAMP_WAIT_ODD) # 파지(클램프) 완료 대기 if (EMO(0, 0)): continue - robot.linear_move_extend([0, 0, -694.816,0,0,0], 1, False, speed, acc, 0.1) # 책 떠서 올림 + robot.linear_move_extend([0, 0, -694.816, 0, 0, 0], 1, False, speed, acc, 0.1) # [픽업] 책을 뜬 채 팔레트 상공 높이까지 상승 write_log("BE: linear_move_extend : [0, 0, -694.816,0,0,0] (lift with book placed on gripper) successfully completed - one_line_wooden_wheel_pallet") is_in_pose() if (EMO(0, 1)): continue - time.sleep(0.3) + time.sleep(BOOK_SETTLE_WAIT) # 책 흔들림 안정화 if centerY > pallet_length / 2: - # 아래 팔레트 집입 자세 + # 아래 팔레트 진입 자세 robot.joint_move_extend(lower_pallet_enter_pose, 0, False, 8000, 3000, 0.1) write_log("BE: joint_move_extend : lower_pallet_enter_pose for odd layer grid 2 and 4 successfully completed - one_line_wooden_wheel_pallet") is_in_pose() @@ -2274,7 +2458,7 @@ def one_line_wooden_wheel_pallet(): get_distance_to_base(wooden_wheel_pallet_max_z_pos) else: - # 위에 팔레트 집입 자세 + # 위에 팔레트 진입 자세 robot.joint_move_extend(lower_pallet_enter_pose1, 0, False, 8000, 3000, 0.1) write_log("BE: joint_move_extend : lower_pallet_enter_pose1 for odd layer grid 1 and 3 successfully completed - one_line_wooden_wheel_pallet") is_in_pose() @@ -2314,15 +2498,16 @@ def one_line_wooden_wheel_pallet(): # @@@@@@@@@@@@@@@@@@@current_layer_height 에 z값 저장 (z값은 is_in_pose 함수에서 가져옴, )@@@@@@@@@@@@@@@@@@@@@ current_layer_height = z_pos - robot.set_digital_output(IO_CABINET, 0, 0) # 그리퍼 수직 off + # ── [적재 릴리즈] 수직 그리퍼 해제 → 푸셔 고정 → 포크 인출 → 푸셔 해제 ── + robot.set_digital_output(IO_CABINET, 0, 0) # [적재] 수직 그리퍼 OFF (책을 적재면에 내려놓음) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (gripper vertical cylinder off after put on pallet) successfully completed - one_line_wooden_wheel_pallet") - time.sleep(0.3) + time.sleep(RELEASE_WAIT) # (기존 0.3/0.2) 수직 그리퍼 후퇴 대기 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 1) # 그리퍼 수평 on + robot.set_digital_output(IO_CABINET, 1, 1) # [적재] 수평 푸셔 전진: 포크 인출 시 책이 딸려 나오지 않게 고정 write_log("BE: set_digital_output(IO_CABINET, 1, 1) (gripper parallel cylinder on after put on pallet) successfully completed - one_line_wooden_wheel_pallet") - time.sleep(1) + time.sleep(PUSH_HOLD_WAIT) # (기존 1.0s) 푸셔 스트로크 완료 대기 if (EMO(1, 1)): continue @@ -2332,7 +2517,7 @@ def one_line_wooden_wheel_pallet(): if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 0) # 그리퍼 수평 off + robot.set_digital_output(IO_CABINET, 1, 0) # [적재] 수평 푸셔 OFF → 홈 복귀 후 다음 사이클 write_log("BE: set_digital_output(IO_CABINET, 1, 0) (after lift up turn OFF gripper parallel cylinder) successfully completed - one_line_wooden_wheel_pallet") if (EMO(1, 0)): continue @@ -2353,7 +2538,7 @@ def one_line_wooden_wheel_pallet(): is_move_pose = True - else: # 짝수 번째 레이어 (그리퍼 돌려서 책 들기) + else: # 짝수 번째 층(2,4,6...): 그리퍼를 돌려 반대 방향 픽업(맞물림 적재로 하중 분산) write_log("BE: load book on pallet's else condition started - one_line_wooden_wheel_pallet") current_data_list = list(book_center_positions) current_data = current_data_list[current_data_index] @@ -2361,35 +2546,36 @@ def one_line_wooden_wheel_pallet(): centerX = current_data['x'] centerY = current_data['y'] + # ── [픽업 시퀀스 시작] 그리퍼 2개(수평/수직) OFF 재확인 → 후퇴 대기 → 스태커 진입 ── robot.set_digital_output(IO_CABINET, 1, 0) write_log("BE: set_digital_output(IO_CABINET, 1, 0) (for safety purposes : parallel gripper cylinder off) successfully completed - one_line_wooden_wheel_pallet") robot.set_digital_output(IO_CABINET, 0, 0) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (for safety purposes : vertical gripper cylinder off) successfully completed - one_line_wooden_wheel_pallet") - time.sleep(save_time) + time.sleep(GRIPPER_RETRACT_WAIT) # (기존 save_time=2.0s) 실린더 후퇴 확인용 대기 - robot.linear_move_extend([194.344,0,0,0,0,0], 1, False, 8000, 3000, 0.1) # (-386.213) --> -387.213으로 하기 -->-384.208 + robot.linear_move_extend([194.344, 0, 0, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [픽업] 스태커로 수평 진입 (짝수층: 반대 방향에서 진입) write_log("BE: linear_move_extend : [194.344,0,0,0,0,0] (move toward stacker to grip book) successfully completed - one_line_wooden_wheel_pallet") is_in_pose() if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 0, 1) # 그리퍼 수직 on + robot.set_digital_output(IO_CABINET, 0, 1) # [픽업] 수직 그리퍼 ON → 책 파지 write_log("BE: set_digital_output(IO_CABINET, 0, 1) (turn ON vertical gripper cylinder to grip book) successfully completed - one_line_wooden_wheel_pallet") - time.sleep(0.1) + time.sleep(GRIP_CLAMP_WAIT_EVEN) # 파지(클램프) 완료 대기 if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,-703.816,0,0,0], 1, False, speed, acc, 0.1) # (-927.623) + robot.linear_move_extend([0, 0, -703.816, 0, 0, 0], 1, False, speed, acc, 0.1) # [픽업] 책을 뜬 채 팔레트 상공 높이까지 상승 write_log("BE: linear_move_extend : [0,0,-703.816,0,0,0] (lift with book placed on gripper) successfully completed - one_line_wooden_wheel_pallet") is_in_pose() if (EMO(0, 1)): continue - time.sleep(0.3) + time.sleep(BOOK_SETTLE_WAIT) # 책 흔들림 안정화 if centerY > pallet_length /2: - robot.joint_move_extend(lower_pallet_enter_pose, 0, False, 5, 1, 0.1) # 만약 책이 미끄러진다면 속도 조절 둘다 + robot.joint_move_extend(lower_pallet_enter_pose, 0, False, 5, 1, 0.1) # 짝수층은 파지 자세 특성상 저속 진입(책 미끄러짐 방지). 미끄러지면 speed/acc를 더 낮출 것 write_log("BE: joint_move_extend : lower_pallet_enter_pose for even layer 2 and 4 successfully completed - one_line_wooden_wheel_pallet") is_in_pose() if (EMO(0, 1)): @@ -2465,15 +2651,16 @@ def one_line_wooden_wheel_pallet(): # @@@@@@@@@@@@@@@@@@@current_layer_height 에 z값 저장 (z값은 is_in_pose 함수에서 가져옴, )@@@@@@@@@@@@@@@@@@@@@ current_layer_height = z_pos - robot.set_digital_output(IO_CABINET, 0, 0) # 그리퍼 수직 off + # ── [적재 릴리즈] 수직 그리퍼 해제 → 푸셔 고정 → 포크 인출 → 푸셔 해제 ── + robot.set_digital_output(IO_CABINET, 0, 0) # [적재] 수직 그리퍼 OFF (책을 적재면에 내려놓음) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (gripper vertical cylinder off after put on pallet) successfully completed - one_line_wooden_wheel_pallet") - time.sleep(0.2) + time.sleep(RELEASE_WAIT) # (기존 0.3/0.2) 수직 그리퍼 후퇴 대기 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 1) # 그리퍼 수평 on + robot.set_digital_output(IO_CABINET, 1, 1) # [적재] 수평 푸셔 전진: 포크 인출 시 책이 딸려 나오지 않게 고정 write_log("BE: set_digital_output(IO_CABINET, 1, 1) (gripper parallel cylinder on after put on pallet) successfully completed - one_line_wooden_wheel_pallet") - time.sleep(1) + time.sleep(PUSH_HOLD_WAIT) # (기존 1.0s) 푸셔 스트로크 완료 대기 if (EMO(1, 1)): continue @@ -2483,7 +2670,7 @@ def one_line_wooden_wheel_pallet(): if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 0) # 그리퍼 수평 off + robot.set_digital_output(IO_CABINET, 1, 0) # [적재] 수평 푸셔 OFF → 홈 복귀 후 다음 사이클 write_log("BE: set_digital_output(IO_CABINET, 1, 0) (after lift up turn OFF gripper parallel cylinder) successfully completed - one_line_wooden_wheel_pallet") if (EMO(1, 0)): continue @@ -2505,6 +2692,10 @@ def one_line_wooden_wheel_pallet(): def one_line_iron_wheel_pallet(): + """[한줄쌓기] 철제 바퀴 팔레트: one_line_wooden_wheel_pallet() 과 동일 시퀀스. + + 차이점: 최대 하강 z(884)와 그리드 진입 자세/좌표 오프셋이 철제 팔레트 기준. + """ write_log("BE: one_line_iron_wheel_pallet function started") global action, immediately_take_book global emergency_stop, protected_stop_stat @@ -2562,11 +2753,14 @@ def one_line_iron_wheel_pallet(): current_layer_height = 0 while True: + # busy-wait 방지: 짧게 쉬어 상태 폴링 스레드(GIL)가 센서값을 제때 갱신하도록 함 + time.sleep(IDLE_LOOP_INTERVAL) if (EMO(0, 0)): is_move_pose = True continue + # UI '동작 종료' → 진행 상태 초기화 후 루프 종료 (스태커/컨베이어 정지) if action == 'robot_finish_action': action = None current_layer_index = 0 @@ -2623,6 +2817,8 @@ def one_line_iron_wheel_pallet(): write_log("BE: current_layer_height > oneline_wheel_max_current_layer_height successfully completed - one_line_iron_wheel_pallet") break + # ── 픽업 대기 자세 잡기 (사이클 완료/비상 복구 후 1회씩 수행) ── + # 홈 → 픽업 준비 자세 → 픽업 대기 높이 하강 순서로 이동해 두고 책을 기다린다. if is_move_pose == True and is_emo_disabled == True: is_move_pose = False if current_layer_index % 2 == 0: @@ -2638,7 +2834,7 @@ def one_line_iron_wheel_pallet(): if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,666.816,0,0,0], 1,False, 8000, 3000, 0.1) # 공중에서 z축 내리기(-247.633) + robot.linear_move_extend([0, 0, 666.816, 0, 0, 0], 1, False, 8000, 3000, 0.1) # 픽업 대기 높이까지 하강(스태커 옆 공중 대기) write_log("BE: linear_move_extend : [0,0,666.816,0,0,0] (ready pose) successfully completed - one_line_iron_wheel_pallet") is_in_pose() if (EMO(0, 0)): @@ -2656,16 +2852,19 @@ def one_line_iron_wheel_pallet(): if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,681.817,0,0,0], 1, False, 8000, 3000, 0.1) # (-273.633) -253.633 --> -249.633 + robot.linear_move_extend([0, 0, 681.817, 0, 0, 0], 1, False, 8000, 3000, 0.1) # 픽업 대기 높이까지 하강(짝수층 대기 위치) write_log("BE: linear_move_extend : [0,0,681.817,0,0,0] (ready pose) successfully completed - one_line_iron_wheel_pallet") is_in_pose() if (EMO(0, 0)): continue + # ── 픽업 시작 조건 ── + # (책 묶음 준비 AND 스태커 수직 실린더 완전 하강 AND 로봇 정상) + # 또는 (UI '즉시 픽업' AND 수직 실린더 상승 아님 AND 로봇 정상) if (((stacker_book_ready_sensor == 1) and (stacker_vertical_cyl_sensor == 0) and (stacker_vertical_cyl_down_sensor == 1)) and (emergency_stop == 0 or protected_stop_stat == 0) and (power_on_stat == 1 and enabled_stat == 1)) or (((immediately_take_book == 'robot_immediately_take_book') and (stacker_vertical_cyl_sensor == 0)) and (emergency_stop == 0 or protected_stop_stat == 0) and (power_on_stat == 1 and enabled_stat == 1)): immediately_take_book = None - if current_layer_index % 2 == 0: # 홀수 번째 레이어 + if current_layer_index % 2 == 0: # 홀수 번째 층(1,3,5...): 기본 방향으로 픽업 write_log("BE: load book on pallet's if condition started - one_line_iron_wheel_pallet") current_data_list = list(book_center_positions) current_data = current_data_list[current_data_index] @@ -2673,35 +2872,36 @@ def one_line_iron_wheel_pallet(): centerX = current_data['x'] centerY = current_data['y'] + # ── [픽업 시퀀스 시작] 그리퍼 2개(수평/수직) OFF 재확인 → 후퇴 대기 → 스태커 진입 ── robot.set_digital_output(IO_CABINET, 1, 0) write_log("BE: set_digital_output(IO_CABINET, 1, 0) (for safety purposes : parallel gripper cylinder off) successfully completed - one_line_iron_wheel_pallet") robot.set_digital_output(IO_CABINET, 0, 0) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (for safety purposes : vertical gripper cylinder off) successfully completed - one_line_iron_wheel_pallet") - time.sleep(save_time) + time.sleep(GRIPPER_RETRACT_WAIT) # (기존 save_time=2.0s) 실린더 후퇴 확인용 대기 - robot.linear_move_extend([-298.306, 0, 0,0,0,0], 1, False, 8000, 3000, 0.1) # x축 -해서 책 집을 위치로 가기 (-67.025) + robot.linear_move_extend([-298.306, 0, 0, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [픽업] 스태커로 수평 진입 (포크가 책 밑으로 들어감) write_log("BE: linear_move_extend : [-298.306, 0, 0,0,0,0] (move toward stacker to grip book) successfully completed - one_line_iron_wheel_pallet") is_in_pose() if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 0, 1) # 그리퍼 수직 작동 + robot.set_digital_output(IO_CABINET, 0, 1) # [픽업] 수직 그리퍼 ON → 책 파지 write_log("BE: set_digital_output(IO_CABINET, 0, 1) (turn ON vertical gripper cylinder to grip book) successfully completed - one_line_iron_wheel_pallet") - time.sleep(0.3) + time.sleep(GRIP_CLAMP_WAIT_ODD) # 파지(클램프) 완료 대기 if (EMO(0, 0)): continue - robot.linear_move_extend([0, 0, -694.816,0,0,0], 1, False, speed, acc, 0.1) # 책 떠서 올림 + robot.linear_move_extend([0, 0, -694.816, 0, 0, 0], 1, False, speed, acc, 0.1) # [픽업] 책을 뜬 채 팔레트 상공 높이까지 상승 write_log("BE: linear_move_extend : [0, 0, -694.816,0,0,0] (lift with book placed on gripper) successfully completed - one_line_iron_wheel_pallet") is_in_pose() if (EMO(0, 1)): continue - time.sleep(0.3) + time.sleep(BOOK_SETTLE_WAIT) # 책 흔들림 안정화 if centerY > pallet_length / 2: - # 아래 팔레트 집입 자세 + # 아래 팔레트 진입 자세 robot.joint_move_extend(lower_pallet_enter_pose, 0, False, 8000, 3000, 0.1) write_log("BE: joint_move_extend : lower_pallet_enter_pose for odd layer grid 2 and 4 successfully completed - one_line_iron_wheel_pallet") is_in_pose() @@ -2739,7 +2939,7 @@ def one_line_iron_wheel_pallet(): get_distance_to_base(iron_wheel_pallet_max_z_pos) else: - # 위에 팔레트 집입 자세 + # 위에 팔레트 진입 자세 robot.joint_move_extend(lower_pallet_enter_pose1, 0, False, 8000, 3000, 0.1) write_log("BE: joint_move_extend : lower_pallet_enter_pose1 odd layer grid 1 and 3 successfully completed - one_line_iron_wheel_pallet") is_in_pose() @@ -2779,15 +2979,16 @@ def one_line_iron_wheel_pallet(): # @@@@@@@@@@@@@@@@@@@current_layer_height 에 z값 저장 (z값은 is_in_pose 함수에서 가져옴, )@@@@@@@@@@@@@@@@@@@@@ current_layer_height = z_pos - robot.set_digital_output(IO_CABINET, 0, 0) # 그리퍼 수직 off + # ── [적재 릴리즈] 수직 그리퍼 해제 → 푸셔 고정 → 포크 인출 → 푸셔 해제 ── + robot.set_digital_output(IO_CABINET, 0, 0) # [적재] 수직 그리퍼 OFF (책을 적재면에 내려놓음) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (gripper vertical cylinder off after put on pallet) successfully completed - one_line_iron_wheel_pallet") - time.sleep(0.3) + time.sleep(RELEASE_WAIT) # (기존 0.3/0.2) 수직 그리퍼 후퇴 대기 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 1) # 그리퍼 수평 on + robot.set_digital_output(IO_CABINET, 1, 1) # [적재] 수평 푸셔 전진: 포크 인출 시 책이 딸려 나오지 않게 고정 write_log("BE: set_digital_output(IO_CABINET, 1, 1) (gripper parallel cylinder on after put on pallet) successfully completed - one_line_iron_wheel_pallet") - time.sleep(1) + time.sleep(PUSH_HOLD_WAIT) # (기존 1.0s) 푸셔 스트로크 완료 대기 if (EMO(1, 1)): continue @@ -2797,7 +2998,7 @@ def one_line_iron_wheel_pallet(): if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 0) # 그리퍼 수평 off + robot.set_digital_output(IO_CABINET, 1, 0) # [적재] 수평 푸셔 OFF → 홈 복귀 후 다음 사이클 write_log("BE: set_digital_output(IO_CABINET, 1, 0) (after lift up turn OFF gripper parallel cylinder) successfully completed - one_line_iron_wheel_pallet") if (EMO(1, 0)): continue @@ -2818,7 +3019,7 @@ def one_line_iron_wheel_pallet(): is_move_pose = True - else: # 짝수 번째 레이어 (그리퍼 돌려서 책 들기) + else: # 짝수 번째 층(2,4,6...): 그리퍼를 돌려 반대 방향 픽업(맞물림 적재로 하중 분산) write_log("BE: load book on pallet's else condition started - one_line_iron_wheel_pallet") current_data_list = list(book_center_positions) current_data = current_data_list[current_data_index] @@ -2826,35 +3027,36 @@ def one_line_iron_wheel_pallet(): centerX = current_data['x'] centerY = current_data['y'] + # ── [픽업 시퀀스 시작] 그리퍼 2개(수평/수직) OFF 재확인 → 후퇴 대기 → 스태커 진입 ── robot.set_digital_output(IO_CABINET, 1, 0) write_log("BE: set_digital_output(IO_CABINET, 1, 0) (for safety purposes : parallel gripper cylinder off) successfully completed - one_line_iron_wheel_pallet") robot.set_digital_output(IO_CABINET, 0, 0) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (for safety purposes : vertical gripper cylinder off) successfully completed - one_line_iron_wheel_pallet") - time.sleep(save_time) + time.sleep(GRIPPER_RETRACT_WAIT) # (기존 save_time=2.0s) 실린더 후퇴 확인용 대기 - robot.linear_move_extend([194.344,0,0,0,0,0], 1, False, 8000, 3000, 0.1) # (-386.213) --> -387.213으로 하기 -->-384.208 + robot.linear_move_extend([194.344, 0, 0, 0, 0, 0], 1, False, 8000, 3000, 0.1) # [픽업] 스태커로 수평 진입 (짝수층: 반대 방향에서 진입) write_log("BE: linear_move_extend : [194.344,0,0,0,0,0] (move toward stacker to grip book) successfully completed - one_line_iron_wheel_pallet") is_in_pose() if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 0, 1) # 그리퍼 수직 on + robot.set_digital_output(IO_CABINET, 0, 1) # [픽업] 수직 그리퍼 ON → 책 파지 write_log("BE: set_digital_output(IO_CABINET, 0, 1) (turn ON vertical gripper cylinder to grip book) successfully completed - one_line_iron_wheel_pallet") - time.sleep(0.1) + time.sleep(GRIP_CLAMP_WAIT_EVEN) # 파지(클램프) 완료 대기 if (EMO(0, 0)): continue - robot.linear_move_extend([0,0,-703.816,0,0,0], 1, False, speed, acc, 0.1) # (-927.623) + robot.linear_move_extend([0, 0, -703.816, 0, 0, 0], 1, False, speed, acc, 0.1) # [픽업] 책을 뜬 채 팔레트 상공 높이까지 상승 write_log("BE: linear_move_extend : [0,0,-703.816,0,0,0] (lift with book placed on gripper) successfully completed - one_line_iron_wheel_pallet") is_in_pose() if (EMO(0, 1)): continue - time.sleep(0.3) + time.sleep(BOOK_SETTLE_WAIT) # 책 흔들림 안정화 if centerY > pallet_length /2: - robot.joint_move_extend(lower_pallet_enter_pose, 0, False, 5, 1, 0.1) # 만약 책이 미끄러진다면 속도 조절 둘다 + robot.joint_move_extend(lower_pallet_enter_pose, 0, False, 5, 1, 0.1) # 짝수층은 파지 자세 특성상 저속 진입(책 미끄러짐 방지). 미끄러지면 speed/acc를 더 낮출 것 write_log("BE: joint_move_extend : lower_pallet_enter_pose for even layer grid 2 and 4 successfully completed - one_line_iron_wheel_pallet") is_in_pose() if (EMO(0, 1)): @@ -2930,15 +3132,16 @@ def one_line_iron_wheel_pallet(): # @@@@@@@@@@@@@@@@@@@current_layer_height 에 z값 저장 (z값은 is_in_pose 함수에서 가져옴, )@@@@@@@@@@@@@@@@@@@@@ current_layer_height = z_pos - robot.set_digital_output(IO_CABINET, 0, 0) # 그리퍼 수직 off + # ── [적재 릴리즈] 수직 그리퍼 해제 → 푸셔 고정 → 포크 인출 → 푸셔 해제 ── + robot.set_digital_output(IO_CABINET, 0, 0) # [적재] 수직 그리퍼 OFF (책을 적재면에 내려놓음) write_log("BE: set_digital_output(IO_CABINET, 0, 0) (gripper vertical cylinder off after put on pallet) successfully completed - one_line_iron_wheel_pallet") - time.sleep(0.2) + time.sleep(RELEASE_WAIT) # (기존 0.3/0.2) 수직 그리퍼 후퇴 대기 if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 1) # 그리퍼 수평 on + robot.set_digital_output(IO_CABINET, 1, 1) # [적재] 수평 푸셔 전진: 포크 인출 시 책이 딸려 나오지 않게 고정 write_log("BE: set_digital_output(IO_CABINET, 1, 1) (gripper parallel cylinder on after put on pallet) successfully completed - one_line_iron_wheel_pallet") - time.sleep(1) + time.sleep(PUSH_HOLD_WAIT) # (기존 1.0s) 푸셔 스트로크 완료 대기 if (EMO(1, 1)): continue @@ -2948,7 +3151,7 @@ def one_line_iron_wheel_pallet(): if (EMO(0, 0)): continue - robot.set_digital_output(IO_CABINET, 1, 0) # 그리퍼 수평 off + robot.set_digital_output(IO_CABINET, 1, 0) # [적재] 수평 푸셔 OFF → 홈 복귀 후 다음 사이클 write_log("BE: set_digital_output(IO_CABINET, 1, 0) (after lift up turn OFF gripper parallel cylinder) successfully completed - one_line_iron_wheel_pallet") if (EMO(1, 0)): continue @@ -2977,6 +3180,11 @@ current_DI_9_status = 0 # jks_status = 0 def robot_get_status_realtime(): + """로봇 상태 폴링 스레드를 시작한다 (모듈 로드 시 1회 호출). + + 0.1초 주기로 get_robot_status()를 읽어 전역 상태(전원/서보/비상정지/DI 센서)를 + 갱신하고, 경광등·컨베이어 인터록과 비상정지 해제 후 홈 복귀를 담당한다. + """ def get_status_thread(): global power_on_stat global enabled_stat @@ -3030,16 +3238,21 @@ def robot_get_status_realtime(): current_layer_height_oneLine = 0 + # 경광등 마지막 전송값 캐시 (값이 바뀔 때만 DO를 쓰기 위함) + last_lamp = None + lamp = None + lamp_refresh_count = 0 + while True: try: status = robot.get_robot_status() - power_on_stat = status[1][2] - enabled_stat = status[1][3] - emergency_stop = status[1][23] - protected_stop_stat = status[1][5] + power_on_stat = status[1][2] # 전원 ON 여부 + enabled_stat = status[1][3] # 서보 enable 여부 + emergency_stop = status[1][23] # 비상정지 눌림 여부 + protected_stop_stat = status[1][5] # 보호정지(충돌 감지) 여부 current_DI_9_status = status[1][11][0] # 스태커 수직 반사판 센서 - stacker_book_ready_sensor = status[1][11][1] # 스태커 수평 반사판 샌서 + stacker_book_ready_sensor = status[1][11][1] # 스태커 수평 반사판 센서 stacker_vertical_cyl_sensor = status[1][11][2] # 스태커 수직 실린더 위에 센서 stacker_vertical_cyl_down_sensor = status[1][11][3] # 스태커 수직 실린더 아래 센서 @@ -3072,28 +3285,32 @@ def robot_get_status_realtime(): (wheel_max_current_layer_height < current_layer_height_oneLine + 130 < wheel_max_current_layer_height + 130)) and (pallet_height == wooden_wheel_pallet_height or pallet_height == iron_wheel_pallet_height)) or (flat_max_current_layer_height < current_layer_height + 130 < flat_max_current_layer_height + 130)): - robot.set_digital_output(IO_CABINET, 2, 0) # 경광등 주황색 off - robot.set_digital_output(IO_CABINET, 3, 0) # 경광등 초록색 off - robot.set_digital_output(IO_CABINET, 4, 1) # 경광등 빨간색 on + lamp = (0, 0, 1) # 빨강: 종료/최대 적재 도달/비상정지 elif((enabled_stat == 1) and (emergency_stop == 0 or protected_stop_stat == 0) and (((wheel_max_current_layer_height - 130 < current_layer_height + 130 < wheel_max_current_layer_height) or (wheel_max_current_layer_height - 130 < current_layer_height_oneLine + 130 < wheel_max_current_layer_height)) and (pallet_height == wooden_wheel_pallet_height or pallet_height == iron_wheel_pallet_height)) or (flat_max_current_layer_height - 130 < current_layer_height + 130 < flat_max_current_layer_height)): - robot.set_digital_output(IO_CABINET, 2, 1) # 경광등 주황색 on - robot.set_digital_output(IO_CABINET, 3, 0) # 경광등 초록색 off - robot.set_digital_output(IO_CABINET, 4, 0) # 경광등 빨간색 off + lamp = (1, 0, 0) # 주황: 마지막 층 적재 근접 예고 elif(enabled_stat == 1 and (robot_start_clicked == True) and (emergency_stop == 0 or protected_stop_stat == 0)): - robot.set_digital_output(IO_CABINET, 2, 0) # 경광등 주황색 off - robot.set_digital_output(IO_CABINET, 3, 1) # 경광등 초록색 on - robot.set_digital_output(IO_CABINET, 4, 0) # 경광등 빨간색 off + lamp = (0, 1, 0) # 초록: 정상 운전 중 elif(emergency_stop == 1 or protected_stop_stat == 1): - robot.set_digital_output(IO_CABINET, 2, 0) # 경광등 주황색 off - robot.set_digital_output(IO_CABINET, 3, 0) # 경광등 초록색 off - robot.set_digital_output(IO_CABINET, 4, 1) # 경광등 빨간색 on + lamp = (0, 0, 1) # 빨강: 종료/최대 적재 도달/비상정지 + # 경광등 쓰기: 같은 값을 0.1초마다 재전송하면 로봇 통신만 점유하므로 + # 값이 바뀔 때만 쓰고, 3초(30주기)마다 한 번 재전송하여 상태를 복구한다. + lamp_refresh_count += 1 + if lamp is not None and (lamp != last_lamp or lamp_refresh_count >= 30): + lamp_refresh_count = 0 + last_lamp = lamp + robot.set_digital_output(IO_CABINET, 2, lamp[0]) # 경광등 주황 + robot.set_digital_output(IO_CABINET, 3, lamp[1]) # 경광등 초록 + robot.set_digital_output(IO_CABINET, 4, lamp[2]) # 경광등 빨강 + + # ── 비상정지/보호정지 발생: 스태커·컨베이어 정지, 복구 플래그 세팅 ── if(emergency_stop == 1 or protected_stop_stat == 1): - write_log("BE: emergency or protected enabled") + if flag == 0: + write_log("BE: emergency or protected enabled") # 최초 1회만 기록 (기존: 0.1s마다 반복 기록) is_emo_disabled = False flag = 1 @@ -3104,6 +3321,7 @@ def robot_get_status_realtime(): robot.set_digital_output(IO_CABINET, 5, 0) write_log("BE: robot.set_digital_output(IO_CABINET, 5, 0) conveyor belt off") + # ── 비상 해제 감지: 안전 높이(-350mm) 리프트 → 홈 복귀 → 스태커 재시작 ── if(flag == 1 and enabled_stat == 1 and (emergency_stop == 0 and protected_stop_stat == 0)): write_log("BE: after emergency or protected disabled") is_emo_disabled = True @@ -3127,9 +3345,16 @@ def robot_get_status_realtime(): def stacker_thread_start_function(): + """스태커(책 정렬대) 실린더 제어 스레드를 시작한다. + + 스태커: 재단기에서 낱권으로 들어오는 책을 수평 실린더로 밀어 정렬하고, + 낱권 감지(DI0) 시 수직 실린더로 한 번 눌러 압축하는 장치. + 책 묶음이 완성(DI1=1)되면 로봇이 가져갈 때까지 대기한다. + """ global stacker_thread_running, stacker_thread_operator + def stacker_thread(): - Write_All_Do([0,0,0,0,0,0,0,0]) + Write_All_Do([0, 0, 0, 0, 0, 0, 0, 0]) # 시퀀스 시작 전 실린더 전체 OFF global emergency_stop global protected_stop_stat global current_DI_9_status @@ -3142,45 +3367,68 @@ def stacker_thread_start_function(): while stacker_thread_running: try: - instrument.write_bit(1, True) # 수평 + instrument.write_bit(1, True) # 수평 실린더 전진 유지 (책 정렬 기본 상태) if stacker_book_ready_sensor == 1: - time.sleep(2) # 이 센서가 활성화 되면 로봇이 가져가야하기 때문에 time을 주어야 할 것 같음. + # 묶음 완성 → 로봇이 가져갈 때까지 스태커는 쉰다. + # (기존 2.0s → 0.5s: 픽업 직후 스태커 재개가 최대 1.5s 빨라짐) + time.sleep(STACKER_BACKOFF) elif current_DI_9_status == 1 and stacker_book_ready_sensor == 0 and (emergency_stop == 0 or protected_stop_stat == 0): + # 낱권 감지 → 1회 압축 시퀀스: + # 수직 누름 → 수평 후퇴(다음 책 유입 공간 확보) → 수평 재전진 → 수직 해제 print(f"스태커 스레드 current_DI_9_status: {current_DI_9_status}") - time.sleep(0.5) - instrument.write_bit(0, True) - time.sleep(0.05) - # robot.set_digital_output(IO_CABINET, 5, 0) # 동력 off - instrument.write_bit(1, False) - time.sleep(1.2) - instrument.write_bit(1, True) - time.sleep(0.2) - instrument.write_bit(0, False) - # robot.set_digital_output(IO_CABINET, 5, 1) # 동력 ON + time.sleep(STACKER_PRE_PRESS_WAIT) + instrument.write_bit(0, True) # 수직 실린더 누름 + time.sleep(STACKER_PRESS_SETTLE) + instrument.write_bit(1, False) # 수평 실린더 후퇴 (다음 책 유입 공간 확보) + time.sleep(STACKER_SIDE_RETRACT) + instrument.write_bit(1, True) # 수평 실린더 재전진 (정렬) + time.sleep(STACKER_SIDE_PUSH) + instrument.write_bit(0, False) # 수직 실린더 해제 if emergency_stop == 1 or protected_stop_stat == 1: - instrument.write_bit(1, False) + instrument.write_bit(1, False) # 비상 시 수평 실린더 해제 - except Exception as e: - pass + except Exception: + pass # Modbus 일시 오류는 무시하고 다음 주기에 재시도 finally: - time.sleep(0.1) + time.sleep(0.1) # 스태커 폴링 주기 stacker_thread_running = True stacker_thread_operator = threading.Thread(target=stacker_thread, daemon=True) stacker_thread_operator.start() def is_in_pose(): - time.sleep(0.1) + """직전에 보낸 non-block 모션 명령이 목표 위치에 도달할 때까지 대기한다. + + - 명령 직후에는 컨트롤러가 '이동 시작'을 아직 반영하지 못해 이전 위치 기준으로 + in_pos=1 을 돌려줄 수 있으므로, IN_POS_GUARD_TIME 만큼 기다린 뒤 폴링을 시작한다. + - 통신 오류(err != 0) 시에는 '미도달'로 간주하고 계속 재시도한다. + (도달 확인 전에 다음 동작(그리퍼 작동 등)으로 넘어가지 않기 위한 안전장치) + """ + time.sleep(IN_POS_GUARD_TIME) # (기존 0.1s) in-pos 오판 방지 가드 in_pose = 0 while not in_pose: err, in_pose = robot.is_in_pos() + if err != 0: + in_pose = 0 # 통신 오류 → 미도달로 간주하고 재시도 + if not in_pose: + time.sleep(IN_POS_POLL_INTERVAL) # busy-wait 방지 (기존: 간격 없이 연속 폴링) -z_pos = 0 +z_pos = 0 # 높이 탐색 결과(현재 적재면의 z값). 메인 루프가 current_layer_height 로 사용 def get_distance_to_base(max_z_pos): + """적재 지점 상공에서 적재면 근처까지 하강한 뒤, 접촉 탐색으로 실제 적재 높이를 측정한다. + + max_z_pos: 해당 팔레트 바닥까지의 최대 하강 거리(mm). + 동작 순서: + 1) 진행 상태 json에서 현재 층수/누적 높이를 읽음 + 2) 2층 이상이면 '실측 누적 높이', 미만이면 '계산 높이(층수 x 130mm)' 기준으로 + 적재면 약 130mm 위까지 고속 하강 + 3) 컨트롤러 저장 JKS 프로그램 'pallet_test'로 접촉 탐색 후 + 실측 z값을 전역 z_pos 에 저장 (책 눌림량이 매번 달라 실측값을 사용) + """ global btn_id global z_pos - + if btn_id == "normalButton": if os.path.exists(cache_json_file_path) and os.path.getsize(cache_json_file_path) > 0: with open(cache_json_file_path, 'r', encoding='utf-8') as file: @@ -3228,30 +3476,47 @@ def get_distance_to_base(max_z_pos): is_in_pose() + # ── 적재면 접촉 탐색: 컨트롤러 저장 JKS 프로그램 'pallet_test' 실행 ── robot.program_load("pallet_test") - robot.get_loaded_program() print(robot.get_loaded_program()) robot.program_run() while True: default_program_status = robot.get_program_state() - jks_status = default_program_status[1] - print(f"jks_statusL {jks_status}") + jks_status = default_program_status[1] # 0=종료, 1=실행중, 2=일시정지 if jks_status == 0: - print("jsk 실행 여부") - print(jks_status) - joint_pos = robot.get_joint_position() - cartesian_pos = robot.kine_forward(joint_pos[1]) + # 탐색이 끝난 지점의 현재 z값을 실측 적재 높이로 기록. + # 통신 오류로 관절값이 오염되면 다음 층 적재 높이가 통째로 틀어지므로 + # 정상 수신(err==0)될 때까지 재시도한다. + while True: + err_joint, joint_val = robot.get_joint_position() + if err_joint == 0: + break + time.sleep(IN_POS_POLL_INTERVAL) + cartesian_pos = robot.kine_forward(joint_val) z_pos = -(cartesian_pos[1][2]) write_log(f"BE: z_pos : {z_pos} successfully completed - get_distance_to_base") break + time.sleep(JKS_POLL_INTERVAL) # busy-wait/과다 print 방지 (기존: 간격 없이 폴링하며 매회 print) def EMO(dio_number, status): - if (emergency_stop == 1 or protected_stop_stat == 1) and (power_on_stat == 0 and enabled_stat == 0): # 긴급 정지 버튼 눌림 + """비상정지/보호정지 여부를 확인하고, 정지 중이면 그리퍼 DO를 지정 상태로 유지한다. + + 적재 시퀀스 곳곳에서 `if (EMO(x, y)): continue` 형태로 호출된다. + - 반환 True : 정지 중 → 호출부는 현재 사이클을 중단하고 메인 루프 처음으로 복귀 + - 반환 False : 정상 → 시퀀스 계속 진행 + dio_number/status 는 정지 시점에 유지할 그리퍼 상태: + 예) EMO(0, 1) = 책을 물고 있는 구간 → 수직 그리퍼 ON 유지 (책 낙하 방지) + EMO(0, 0) = 빈 손 구간 → 수직 그리퍼 OFF 유지 + """ + if (emergency_stop == 1 or protected_stop_stat == 1) and (power_on_stat == 0 and enabled_stat == 0): robot.set_digital_output(IO_CABINET, dio_number, status) - robot.set_digital_output(IO_CABINET, 3, 0) # 경광등 초록색 off + robot.set_digital_output(IO_CABINET, 3, 0) # 경광등 초록 OFF + time.sleep(0.1) # 정지 중 메인 루프의 고속 반복 호출로 컨트롤러 통신이 몰리는 것 방지 return True - + return False + +# 모듈 로드 시점부터 상태 폴링 스레드 가동 (앱 실행 즉시 센서/상태 감시 시작) robot_get_status_realtime() \ No newline at end of file diff --git a/app.py b/app.py index 0532a1a..5c17e1a 100644 --- a/app.py +++ b/app.py @@ -1,113 +1,70 @@ +"""팔레타이징 앱 진입점. + +Flask(백엔드) + pywebview(데스크톱 창) + waitress(WSGI 서버) 구성. +로봇 제어 로직과 HTTP API 는 전부 api_routes.py 에 있다. +PyInstaller 로 빌드 시(frozen) 템플릿 경로가 임시 폴더(_MEIPASS)로 바뀌는 것을 처리한다. +""" from flask import Flask, render_template, request, jsonify import sys import os -from api_routes import api_routes +from api_routes import api_routes import webview from waitress import serve +# PyInstaller 실행파일(frozen)이면 압축 해제된 임시 폴더에서 템플릿을 찾는다 if getattr(sys, 'frozen', False): template_folder = os.path.join(sys._MEIPASS, 'templates') app = Flask(__name__, template_folder=template_folder, static_url_path='/static') else: app = Flask(__name__, static_url_path='/static') +app.register_blueprint(api_routes) # 로봇 제어 API 라우트 등록 -#app = Flask(__name__, static_url_path='/static') -app.register_blueprint(api_routes) # 분리된 API 라우트 등록 +# ── 화면 라우트 ────────────────────────────────────────────── @app.route('/') def index(): + """메인 화면: 팔레트 선택/적재 배치 설정.""" return render_template('layer.html') @app.route('/progress') def progess(): + """일반 적재 진행 현황 화면.""" return render_template('progress.html') - + @app.route('/oneLineProgress') def oneLineProgess(): + """한줄쌓기 진행 현황 화면.""" return render_template('oneLineProgress.html') -# 종료 함수 + +# ── 종료 처리 ──────────────────────────────────────────────── def shutdown_waitress_server(): + """waitress 서버 및 프로세스 전체 종료.""" print("Shutting down the server...") os._exit(0) def destroy(window): + """pywebview 창 닫기.""" print('Destroying window..') window.destroy() print('Destroyed!') @app.route('/shutdown', methods=['POST']) def shutdown(): + """UI 종료 버튼: 창 닫고 서버/프로세스 종료.""" destroy(test) shutdown_waitress_server() return 'Server shutting down' -port=5000 -# if __name__ == "__main__": -# app.run(port=port, debug=True) +port = 5000 +# pywebview 가 Flask 앱을 감싸는 데스크톱 창 생성 test = webview.create_window('Palletizing', app) + if __name__ == "__main__": + # webview.start()가 창이 닫힐 때까지 블록되고, + # 창이 닫힌 뒤에는 waitress 단독 서버로 계속 서비스한다(0.0.0.0:5000). webview.start() serve(app, host="0.0.0.0", port=port) - - -# from flask import Flask, render_template, request -# import sys -# import os -# from api_routes import api_routes -# import webview -# from waitress import serve -# import threading - -# # Flask 애플리케이션 설정 -# if getattr(sys, 'frozen', False): -# template_folder = os.path.join(sys._MEIPASS, 'templates') -# app = Flask(__name__, template_folder=template_folder, static_url_path='/static') -# else: -# app = Flask(__name__, static_url_path='/static') - -# app.register_blueprint(api_routes) - -# @app.route('/') -# def index(): -# return render_template('layer.html') - -# @app.route('/progress') -# def progress(): -# return render_template('progress.html') - -# @app.route('/oneLineProgress') -# def oneLineProgress(): -# return render_template('oneLineProgress.html') - -# # 종료 함수 -# def shutdown_server(): -# print("Shutting down the server...") -# os._exit(0) - -# # 종료 라우트 설정 -# @app.route('/shutdown', methods=['POST']) -# def shutdown(): -# print("Shutdown request received.") -# destroy(test) # Webview 윈도우 종료 -# shutdown_server() # Waitress 서버와 프로그램 종료 -# return 'Server shutting down...' - -# # Webview 윈도우 종료 함수 -# def destroy(window): -# print('Destroying window..') -# window.destroy() -# print('Destroyed!') - -# # 서버 실행 -# port = 5000 -# test = webview.create_window('Palletizing', app) - -# if __name__ == "__main__": -# # Webview와 Waitress 서버를 별도 스레드로 실행 -# server_thread = threading.Thread(target=lambda: serve(app, host="0.0.0.0", port=port)) -# server_thread.start() -# webview.start()