docs: 스태커 실제 동작(하부 적층 방식) 기준으로 주석/상수명 정정

현장 확인 결과 스태커는 상부 압축이 아니라 하부 적층(bottom-stacking) 장치:
- 책 인입(DI0) -> 리프터(coil0)가 책을 위로 밀어올림 -> 선반(coil1) 열림/닫힘
  -> 새 책이 스택 맨 아래에 끼워진 채 선반 위에 걸쳐짐 -> 픽업량 도달(DI1) 시 로봇 픽업
- 리프터 상승 중(DI3=0)에는 컨베이어 일시정지 연동

변경 사항 (로직/타이밍 변경 없음, 명칭/주석만):
- 스태커 상수명 정정: LIFT_START_WAIT / SHELF_OPEN_DELAY / SHELF_OPEN_TIME / SHELF_CLOSE_SETTLE
- 헤더 I/O 맵, [스태커 동작 원리] 섹션, 스레드/픽업 조건 주석 전면 정정
- DO0 그리퍼 명칭을 누름판(포크 받침 + 상부 누름 고정) 구조로 명확화

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 16:33:49 +09:00
parent 3d8521ad89
commit 64c333e04d

View File

@@ -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 일시 오류는 무시하고 다음 주기에 재시도