Compare commits

...

3 Commits

Author SHA1 Message Date
76ee9a52b6 docs: 인수인계용 변경 내역서 추가 (원본 대비 변경 사항 정리)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 16:46:23 +09:00
c61f73fa33 build: app.spec에 빌드 명령어 안내 주석 추가, exe 이름을 팔레타이징으로 고정
- pyinstaller app.spec 한 번으로 dist\app\팔레타이징.exe 가 바로 나오도록
  EXE name 변경 (기존: app.exe 빌드 후 수동 이름 변경)
- 빌드용 패키지 설치 명령과 파이썬 버전 참고사항을 spec 상단에 문서화

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 16:40:11 +09:00
64c333e04d 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>
2026-08-07 16:33:49 +09:00
3 changed files with 174 additions and 51 deletions

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

View File

@@ -1,4 +1,20 @@
# -*- mode: python ; coding: utf-8 -*-
# ═════════════════════════════════════════════════════════════════
# 팔레타이징 앱 실행파일 빌드 설정 (PyInstaller)
#
# [빌드 명령어]
# pyinstaller app.spec
# → 산출물: dist\app\팔레타이징.exe (더블클릭 실행, dist\app 폴더째 배포)
#
# [사전 준비 - 빌드용 파이썬에 패키지 설치]
# python -m pip install pyinstaller flask waitress pywebview minimalmodbus pyserial
#
# [참고]
# - SDK/jkrc.pyd 는 Stable ABI(python3.dll)라 파이썬 3.x 버전을 크게 안 탐.
# 단, pywebview(pythonnet)가 최신 파이썬을 아직 지원 안 하면
# Python 3.12 등에서: py -3.12 -m pip install ... / py -3.12 -m PyInstaller app.spec
# - 기존 운영 exe는 Python 3.8로 빌드된 것이었음 (2024년 빌드)
# ═════════════════════════════════════════════════════════════════
a = Analysis(
@@ -21,7 +37,7 @@ exe = EXE(
a.scripts,
[],
exclude_binaries=True,
name='app',
name='팔레타이징', # exe 파일명 (기존에는 name='app'으로 빌드 후 수동으로 이름 변경했었음)
debug=False,
bootloader_ignore_signals=False,
strip=False,

94
변경내역_20260807.md Normal file
View File

@@ -0,0 +1,94 @@
# 팔레타이징 로봇 프로그램 변경 내역서 (인수인계용)
- **대상 설비**: 책 재단기 라인 팔레타이징 로봇 (JAKA) 제어 프로그램
- **작업일**: 2026-08-07
- **작업 목적**: ① 사이클 중 불필요한 대기 제거로 택타임 단축 ② 유지보수를 위한 전체 주석·문서화
- **변경 파일**: api_routes.py(로봇 제어 전체), app.py(진입점 정리), app.spec(빌드 설정), .gitignore
- **중요**: 로봇의 이동 경로·좌표·속도 파라미터는 일절 변경하지 않았음. 대기시간(sleep)과
소프트웨어 구조만 변경. **현장 실기 테스트는 미완료 상태이므로 적용 전 반드시 시운전 필요.**
---
## 1. 원본 코드 및 변경분 위치
| 구분 | 위치 |
|---|---|
| 원본(현장에서 받아온 코드) | git `main` 브랜치 / 로컬 `_backup/` 폴더 |
| 이번 변경분 | git `feature/tact-time-optimization` 브랜치 |
| 저장소 | https://backend-dev.podosee.com/palletizing_corever/palletizing.git |
| 전체 차이 확인 | `git diff main feature/tact-time-optimization` |
---
## 2. 대기시간 조정 — 택타임 단축 (책 1권당 약 2.5~3초 단축 예상)
모든 대기시간은 api_routes.py 상단 **"대기시간 설정 (택타임 튜닝 포인트)" 상수 블록**에
모여 있으며, 문제 발생 시 해당 상수 하나만 주석에 적힌 `[기존값]`으로 되돌리면 된다.
| 항목 | 상수명 | 기존 | 변경 | 비고 |
|---|---|---|---|---|
| 픽업 진입 전 그리퍼 후퇴 대기 | GRIPPER_RETRACT_WAIT | 2.0초 | 0.5초 | 가장 큰 단축 요소 |
| 적재 시 수평 푸셔 유지 대기 | PUSH_HOLD_WAIT | 1.0초 | 0.6초 | |
| 포크 인출 후 푸셔 해제 전 대기 | FORK_EXIT_WAIT | 0.5초 | 0.3초 | |
| 적재 후 수직 그리퍼 해제 대기 | RELEASE_WAIT | 0.3/0.2초 | 0.2초 통일 | 0.2는 짝수층에서 기존 검증값 |
| 모션 도달확인 가드 | IN_POS_GUARD_TIME | 0.1초 | 0.05초 | 1권당 8~10회 호출됨 |
| 스태커 픽업 대기 재확인 주기 | STACKER_BACKOFF | 2.0초 | 0.5초 | 픽업 후 스태커 재개 빨라짐 |
**변경하지 않고 유지한 값** (물리 동작과 직결되어 보수적으로 유지, 추가 튜닝 후보):
파지 대기(홀수층 0.3/짝수층 0.1), 책 흔들림 안정화(0.3), 그리드 진입 저속 파라미터
(grid_speed/grid_acc — 책 미끄러짐 방지 목적), 스태커 실린더 스트로크 대기 4종.
---
## 3. 소프트웨어 구조 개선 (멈칫/지연 원인 제거)
1. **busy-wait 제거**: 메인 루프·도달확인(is_in_pose)·높이탐색 폴링이 대기 간격 없이
돌면서 CPU를 점유해, 센서값을 갱신하는 상태 폴링 스레드가 밀리던 문제 → 폴링 간격
(10~20ms) 추가로 센서 반응성 개선.
2. **높이탐색 루프의 매회 print 제거**: 권당 수백 회 콘솔 출력이 있었고, 윈도우 콘솔이
블록되면(마우스 드래그 등) 로봇 루프 전체가 멈추는 위험이 있었음.
3. **경광등 신호 전송 최적화**: 같은 값을 0.1초마다 3회씩 재전송하던 것을 값이 바뀔
때만 전송(3초마다 1회 재전송으로 자기복구 유지) → 로봇 컨트롤러 통신 부하 제거.
4. **통신 오류 보호(잠재 결함 수정)**: 도달확인·관절값 수신이 통신 오류일 때 오류값을
그대로 믿고 진행하던 부분을 정상 수신까지 재시도하도록 수정 (적재 높이 오염 방지).
5. **비상정지 중 통신 폭주 방지**: 정지 상태에서 무한 고속 반복되던 컨트롤러 호출에 대기 추가.
---
## 4. 주석·문서화 (동작과 무관)
- api_routes.py 상단에 시스템 개요, 로봇 I/O 맵(DO0~DO5, DI0~DI3), 스태커 동작 원리
(하부 적층 방식), 스레드 구조, 진행상태 파일(cache.json/oneLine.json) 설명 추가.
- 전 함수 docstring 및 픽업/적재 시퀀스 단계별 주석 추가.
- 스태커 관련 명칭을 실제 구조(리프터/선반) 기준으로 정정, 오타 수정.
- app.py의 방치된 주석 코드 블록 제거(원본은 git main에 보존).
---
## 5. 실행파일 빌드 방법 (app.spec 상단에도 동일 내용 주석 있음)
```
python -m pip install pyinstaller flask waitress pywebview minimalmodbus pyserial
pyinstaller app.spec
```
- 산출물: `dist\app\팔레타이징.exe`**dist\app 폴더째** 현장 PC에 복사 후 exe 더블클릭 실행.
- 기존 운영 exe는 Python 3.8 빌드였음. SDK의 jkrc.pyd는 Stable ABI라 파이썬 버전 무관하나,
pywebview 설치가 최신 파이썬에서 실패하면 Python 3.12로 빌드할 것.
- **현 운영 exe는 변경 전 코드로 빌드된 것이므로, 이번 변경을 현장 반영하려면 재빌드 필수.**
---
## 6. 적용 절차 및 이상 발생 시 조치 (인수자 필독)
1. 재빌드 → dist\app 폴더 교체 → 소량으로 시운전:
홀수층 1권 → 짝수층(그리퍼 회전) 1권 → 비상정지/해제 복구 → 경광등 3색 확인.
2. 증상별 복원 (api_routes.py 상단 상수 수정만으로 해결):
| 증상 | 조치 |
|---|---|
| 스태커 진입 시 그리퍼 실린더 간섭 | GRIPPER_RETRACT_WAIT 0.5 → 1.0 → 2.0 순서로 복원 |
| 적재 후 포크에 책이 딸려 올라옴 | PUSH_HOLD_WAIT 0.6 → 1.0, FORK_EXIT_WAIT 0.3 → 0.5 |
| 동작이 겹치거나 불안정 | IN_POS_GUARD_TIME 0.05 → 0.1 |
| 픽업 시 책을 놓침 | GRIP_CLAMP_WAIT_ODD/EVEN 값 상향 |
3. 완전 원복: git `main` 브랜치(또는 `_backup/` 원본) 코드로 교체 후 재빌드.