diff --git a/api_routes.py b/api_routes.py index 14fba6d..511c7e3 100644 --- a/api_routes.py +++ b/api_routes.py @@ -1,36 +1,48 @@ """팔레타이징 로봇 제어 API (책 재단기 라인). [시스템 개요] - 재단기에서 나온 책 묶음이 컨베이어를 타고 '스태커(정렬대)'에 쌓이면, - JAKA 로봇이 포크형 그리퍼로 묶음을 떠서 팔레트 위에 층층이 적재한다. + 재단기에서 나온 책이 낱권씩 컨베이어를 타고 '스태커(하부 적층 장치)'로 들어와 + 스택으로 쌓이고, 픽업량만큼 모이면 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=해제 + DO0 : 그리퍼 누름판(수직) 실린더 1=하강(파지) / 0=상승(해제) + (포크가 책 밑을 받친 상태에서 누름판이 위에서 내려와 눌러 고정) DO1 : 그리퍼 수평 실린더(적재 푸셔) 1=전진 / 0=후퇴 DO2 : 경광등 주황 (마지막 층 근접 예고) DO3 : 경광등 초록 (정상 운전) DO4 : 경광등 빨강 (정지/완료/비상) DO5 : 동력장치(컨베이어) 운전 1=운전 / 0=정지 - DI0 : 스태커 수직 반사판 센서 (낱권 감지 → 스태커 1회 압축 동작 트리거) - DI1 : 스태커 책 준비 센서 (묶음 완성 → 로봇 픽업 조건) - DI2 : 스태커 수직 실린더 '위' 센서 - DI3 : 스태커 수직 실린더 '아래' 센서 (완전히 내려가야 로봇 진입 가능) + DI0 : 스태커 책 인입 반사판 센서 (책이 들어오면 가려짐 → 리프트 사이클 트리거) + DI1 : 스태커 스택 준비 반사판 센서 (선반 위 스택이 픽업량만큼 쌓이면 가려짐 → 로봇 픽업) + DI2 : 스태커 리프터(수직 실린더) '위' 센서 + DI3 : 스태커 리프터(수직 실린더) '아래' 센서 + (완전 하강 상태여야 로봇 포크 진입 가능 + 컨베이어 재가동) ※ DI 번호는 get_robot_status() 반환값의 din 배열 인덱스 기준. [Modbus RTU (COM3, slave 1) - 스태커 실린더] - coil 0 : 스태커 수직 실린더 (책 누름) - coil 1 : 스태커 수평 실린더 (책 밀어 정렬) + coil 0 : 스태커 리프터(수직 실린더) 1=상승(책 밀어올림) / 0=하강 + coil 1 : 스태커 선반(수평 실린더) 1=닫힘(스택 받침) / 0=열림(책 통과) + +[스태커 동작 원리 - 하부 적층(bottom-stacking)] + 1) 컨베이어로 들어온 책이 인입 센서(DI0)를 가림 → 리프터가 책을 위로 밀어올림. + 리프터가 하강 위치를 벗어나 있는 동안(DI3=0) 컨베이어는 일시정지된다. + 2) 리프트 중 선반(coil 1)이 잠시 열려, 책(과 그 위의 기존 스택)이 선반 높이를 통과 + 3) 선반이 다시 닫히고 리프터가 내려오면 새 책이 스택 맨 아래에 끼워진 채 + 스택 전체가 선반 위에 걸쳐진다 + 4) 스택이 픽업량(DI1)만큼 쌓이면 로봇이 포크로 스택 밑을 받치고 + 누름판(DO0)으로 잡아 통째로 팔레트에 적재한다 [스레드 구조] - Flask 요청 스레드 : /robot_start, /one_line_run 요청 안에서 적재 메인 루프가 그대로 돌기 때문에 해당 HTTP 요청은 작업 종료까지 블록된다. - get_status_thread : 0.1s 주기로 로봇 상태/센서 폴링, 경광등·컨베이어 제어, 비상정지(EMO) 해제 후 홈 복귀 처리. - - stacker_thread : 스태커 실린더로 책을 정렬/압축하는 반복 시퀀스. + - stacker_thread : 스태커 리프터/선반 실린더로 책을 하부 적층하는 반복 시퀀스. [진행 상태 저장] - static/json/cache.json : 일반 적재 진행 상태 (몇 층/몇 권/현재 적재 높이) @@ -136,17 +148,17 @@ IN_POS_POLL_INTERVAL = 0.01 # [신규] is_in_pos() 폴링 주기 (기존: 간 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 # 수평 재전진 후 수직 해제 전 대기 +STACKER_BACKOFF = 0.5 # [기존 2.0] 스택 픽업 대기(DI1 가려짐) 상태에서 재확인 주기 +STACKER_LIFT_START_WAIT = 0.5 # 책 인입 감지 후 리프트 시작 전 대기 (책이 완전히 들어올 시간) +STACKER_SHELF_OPEN_DELAY = 0.05 # 리프트 시작 후 선반 열기까지 지연 +STACKER_SHELF_OPEN_TIME = 1.2 # 선반 열림 유지 시간 (리프터가 스택을 선반 위로 올리는 동안) +STACKER_SHELF_CLOSE_SETTLE = 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: 책 묶음 준비 센서 +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 @@ -767,8 +779,8 @@ def wooden_wheel_pallet(): continue # ── 픽업 시작 조건 ── - # (책 묶음 준비 AND 스태커 수직 실린더 완전 하강 AND 로봇 정상) - # 또는 (UI '즉시 픽업' AND 수직 실린더 상승 아님 AND 로봇 정상) + # (스택 픽업량 도달(DI1) AND 리프터 완전 하강(DI3) 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 @@ -1253,8 +1265,8 @@ def iron_wheel_pallet(): continue # ── 픽업 시작 조건 ── - # (책 묶음 준비 AND 스태커 수직 실린더 완전 하강 AND 로봇 정상) - # 또는 (UI '즉시 픽업' AND 수직 실린더 상승 아님 AND 로봇 정상) + # (스택 픽업량 도달(DI1) AND 리프터 완전 하강(DI3) 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 @@ -1744,8 +1756,8 @@ def wooden_Flat_pallet(): continue # ── 픽업 시작 조건 ── - # (책 묶음 준비 AND 스태커 수직 실린더 완전 하강 AND 로봇 정상) - # 또는 (UI '즉시 픽업' AND 수직 실린더 상승 아님 AND 로봇 정상) + # (스택 픽업량 도달(DI1) AND 리프터 완전 하강(DI3) 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 @@ -2378,8 +2390,8 @@ def one_line_wooden_wheel_pallet(): continue # ── 픽업 시작 조건 ── - # (책 묶음 준비 AND 스태커 수직 실린더 완전 하강 AND 로봇 정상) - # 또는 (UI '즉시 픽업' AND 수직 실린더 상승 아님 AND 로봇 정상) + # (스택 픽업량 도달(DI1) AND 리프터 완전 하강(DI3) 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 @@ -2859,8 +2871,8 @@ def one_line_iron_wheel_pallet(): continue # ── 픽업 시작 조건 ── - # (책 묶음 준비 AND 스태커 수직 실린더 완전 하강 AND 로봇 정상) - # 또는 (UI '즉시 픽업' AND 수직 실린더 상승 아님 AND 로봇 정상) + # (스택 픽업량 도달(DI1) AND 리프터 완전 하강(DI3) 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 @@ -3251,10 +3263,10 @@ def robot_get_status_realtime(): 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_vertical_cyl_sensor = status[1][11][2] # 스태커 수직 실린더 위에 센서 - stacker_vertical_cyl_down_sensor = status[1][11][3] # 스태커 수직 실린더 아래 센서 + current_DI_9_status = status[1][11][0] # DI0: 스태커 책 인입 반사판 센서 + stacker_book_ready_sensor = status[1][11][1] # DI1: 스태커 스택 준비(픽업량 도달) 센서 + stacker_vertical_cyl_sensor = status[1][11][2] # DI2: 스태커 리프터 '위' 센서 + stacker_vertical_cyl_down_sensor = status[1][11][3] # DI3: 스태커 리프터 '아래' 센서 @@ -3345,11 +3357,12 @@ def robot_get_status_realtime(): def stacker_thread_start_function(): - """스태커(책 정렬대) 실린더 제어 스레드를 시작한다. + """스태커(하부 적층 장치) 실린더 제어 스레드를 시작한다. - 스태커: 재단기에서 낱권으로 들어오는 책을 수평 실린더로 밀어 정렬하고, - 낱권 감지(DI0) 시 수직 실린더로 한 번 눌러 압축하는 장치. - 책 묶음이 완성(DI1=1)되면 로봇이 가져갈 때까지 대기한다. + 동작 원리: 책이 들어오면(DI0) 리프터(coil 0)가 책을 위로 밀어올리고, + 그 사이 선반(coil 1)이 열렸다 닫히면서 책이 선반 위 스택 맨 아래에 끼워진다. + 스택이 픽업량(DI1=1)에 도달하면 로봇이 가져갈 때까지 대기한다. + (자세한 원리는 파일 상단 [스태커 동작 원리] 참고) """ global stacker_thread_running, stacker_thread_operator @@ -3367,27 +3380,27 @@ 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: - # 묶음 완성 → 로봇이 가져갈 때까지 스태커는 쉰다. + # 스택 픽업량 도달 → 로봇이 가져갈 때까지 스태커는 쉰다. # (기존 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회 압축 시퀀스: - # 수직 누름 → 수평 후퇴(다음 책 유입 공간 확보) → 수평 재전진 → 수직 해제 + # 책 인입 감지 → 하부 적층 1사이클: + # 리프터 상승(책+스택을 선반 위로) → 선반 열기 → 선반 닫기 → 리프터 하강 print(f"스태커 스레드 current_DI_9_status: {current_DI_9_status}") - 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) # 수직 실린더 해제 - + time.sleep(STACKER_LIFT_START_WAIT) + instrument.write_bit(0, True) # 리프터 상승 (책을 위로 밀어올림) + time.sleep(STACKER_SHELF_OPEN_DELAY) + instrument.write_bit(1, False) # 선반 열기 (책이 선반 높이를 통과하도록) + time.sleep(STACKER_SHELF_OPEN_TIME) + instrument.write_bit(1, True) # 선반 닫기 (스택을 다시 받침) + time.sleep(STACKER_SHELF_CLOSE_SETTLE) + 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: pass # Modbus 일시 오류는 무시하고 다음 주기에 재시도