Compare commits
5 Commits
main
...
feature/ta
| Author | SHA1 | Date | |
|---|---|---|---|
| 76ee9a52b6 | |||
| c61f73fa33 | |||
| 64c333e04d | |||
| 3d8521ad89 | |||
| 461d5feb01 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -3,3 +3,11 @@ __pycache__/
|
|||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# PyInstaller build outputs
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# local tool / backup folders
|
||||||
|
.claude/
|
||||||
|
_backup/
|
||||||
|
|||||||
814
api_routes.py
814
api_routes.py
File diff suppressed because it is too large
Load Diff
87
app.py
87
app.py
@@ -1,3 +1,9 @@
|
|||||||
|
"""팔레타이징 앱 진입점.
|
||||||
|
|
||||||
|
Flask(백엔드) + pywebview(데스크톱 창) + waitress(WSGI 서버) 구성.
|
||||||
|
로봇 제어 로직과 HTTP API 는 전부 api_routes.py 에 있다.
|
||||||
|
PyInstaller 로 빌드 시(frozen) 템플릿 경로가 임시 폴더(_MEIPASS)로 바뀌는 것을 처리한다.
|
||||||
|
"""
|
||||||
from flask import Flask, render_template, request, jsonify
|
from flask import Flask, render_template, request, jsonify
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -5,109 +11,60 @@ from api_routes import api_routes
|
|||||||
import webview
|
import webview
|
||||||
from waitress import serve
|
from waitress import serve
|
||||||
|
|
||||||
|
# PyInstaller 실행파일(frozen)이면 압축 해제된 임시 폴더에서 템플릿을 찾는다
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
template_folder = os.path.join(sys._MEIPASS, 'templates')
|
template_folder = os.path.join(sys._MEIPASS, 'templates')
|
||||||
app = Flask(__name__, template_folder=template_folder, static_url_path='/static')
|
app = Flask(__name__, template_folder=template_folder, static_url_path='/static')
|
||||||
else:
|
else:
|
||||||
app = Flask(__name__, static_url_path='/static')
|
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('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
|
"""메인 화면: 팔레트 선택/적재 배치 설정."""
|
||||||
return render_template('layer.html')
|
return render_template('layer.html')
|
||||||
|
|
||||||
@app.route('/progress')
|
@app.route('/progress')
|
||||||
def progess():
|
def progess():
|
||||||
|
"""일반 적재 진행 현황 화면."""
|
||||||
return render_template('progress.html')
|
return render_template('progress.html')
|
||||||
|
|
||||||
@app.route('/oneLineProgress')
|
@app.route('/oneLineProgress')
|
||||||
def oneLineProgess():
|
def oneLineProgess():
|
||||||
|
"""한줄쌓기 진행 현황 화면."""
|
||||||
return render_template('oneLineProgress.html')
|
return render_template('oneLineProgress.html')
|
||||||
|
|
||||||
# 종료 함수
|
|
||||||
|
# ── 종료 처리 ────────────────────────────────────────────────
|
||||||
def shutdown_waitress_server():
|
def shutdown_waitress_server():
|
||||||
|
"""waitress 서버 및 프로세스 전체 종료."""
|
||||||
print("Shutting down the server...")
|
print("Shutting down the server...")
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
def destroy(window):
|
def destroy(window):
|
||||||
|
"""pywebview 창 닫기."""
|
||||||
print('Destroying window..')
|
print('Destroying window..')
|
||||||
window.destroy()
|
window.destroy()
|
||||||
print('Destroyed!')
|
print('Destroyed!')
|
||||||
|
|
||||||
@app.route('/shutdown', methods=['POST'])
|
@app.route('/shutdown', methods=['POST'])
|
||||||
def shutdown():
|
def shutdown():
|
||||||
|
"""UI 종료 버튼: 창 닫고 서버/프로세스 종료."""
|
||||||
destroy(test)
|
destroy(test)
|
||||||
shutdown_waitress_server()
|
shutdown_waitress_server()
|
||||||
return 'Server shutting down'
|
return 'Server shutting down'
|
||||||
|
|
||||||
|
|
||||||
port = 5000
|
port = 5000
|
||||||
|
|
||||||
# if __name__ == "__main__":
|
# pywebview 가 Flask 앱을 감싸는 데스크톱 창 생성
|
||||||
# app.run(port=port, debug=True)
|
|
||||||
|
|
||||||
test = webview.create_window('Palletizing', app)
|
test = webview.create_window('Palletizing', app)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
# webview.start()가 창이 닫힐 때까지 블록되고,
|
||||||
|
# 창이 닫힌 뒤에는 waitress 단독 서버로 계속 서비스한다(0.0.0.0:5000).
|
||||||
webview.start()
|
webview.start()
|
||||||
serve(app, host="0.0.0.0", port=port)
|
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()
|
|
||||||
|
|||||||
60
app.spec
Normal file
60
app.spec
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# -*- 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(
|
||||||
|
['app.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[('templates', 'templates'), ('static', 'static'), ('SDK', 'SDK'), ('api_routes.py', 'api_routes.py')],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='팔레타이징', # exe 파일명 (기존에는 name='app'으로 빌드 후 수동으로 이름 변경했었음)
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=True,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name='app',
|
||||||
|
)
|
||||||
94
변경내역_20260807.md
Normal file
94
변경내역_20260807.md
Normal 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/` 원본) 코드로 교체 후 재빌드.
|
||||||
Reference in New Issue
Block a user