commit f06f5e0f6176de9384e155efa867f2e11cf91a94 Author: shKim Date: Fri Jan 2 15:46:19 2026 +0900 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad4c783 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Python 가상환경 +venv/ +web_UI/venv/ + +# Python 캐시 +__pycache__/ +*.py[cod] +*$py.class +*.so + +# 환경 변수 파일 +.env +.env.local + +# IDE 설정 (팀원과 공유하려면 제거) +.vscode/ + +# OS 생성 파일 +.DS_Store +Thumbs.db + +# Arduino 빌드 파일 +*.hex +*.elf +*.bin +build/ diff --git a/LoadCellMeasure/LoadCellMeasure.ino b/LoadCellMeasure/LoadCellMeasure.ino new file mode 100644 index 0000000..a00eb33 --- /dev/null +++ b/LoadCellMeasure/LoadCellMeasure.ino @@ -0,0 +1,226 @@ + +/* + * LoadCellMeasure - 로드셀 무게 측정 코드 + * HX711 앰프 모듈 사용 + * + * 기능: + * 1. 무게 측정 (양수 값으로 증가) + * 2. 시리얼 명령어: 'c' 영점조절, 'p' 일시정지, 'r' 재시작 + * 3. 통신 시작 시 최초 1회 자동 캘리브레이션 + * 4. 측정 속도 조절 가능 ('+' 빠르게, '-' 느리게) + * + * 하드웨어 연결: + * - HX711 VCC -> Arduino 5V + * - HX711 GND -> Arduino GND + * - HX711 DT -> Arduino Pin 3 (DATA_PIN) + * - HX711 SCK -> Arduino Pin 2 (CLOCK_PIN) + */ + +#include "HX711.h" + +// ===== 핀 설정 ===== +const int LOADCELL_DOUT_PIN = 3; // HX711 DT(DATA) 핀 - 아두이노 3 PIN +const int LOADCELL_SCK_PIN = 2; // HX711 SCK(CLOCK) 핀 - 아두이노 2 PIN + +// ===== HX711 인스턴스 ===== +HX711 scale; + +// ===== 설정 변수 ===== +float calibration_factor = -411.17; // 캘리브레이션 팩터 (실제 무게(g) = 90060(Raw 값) ÷ 캘리브레이션 팩터) +int measureDelay = 100; // 측정 간격 (ms) - HX711 10Hz 기준 +const int DELAY_STEP = 10; // 측정 간격 조절 단위 (ms) +const int MIN_DELAY = 10; // 최소 측정 간격 (ms) +const int MAX_DELAY = 2000; // 최대 측정 간격 (ms) + +// ===== 상태 변수 ===== +bool isPaused = false; // 일시정지 상태 +bool isCalibrated = false; // 캘리브레이션 완료 여부 +unsigned long startTime = 0; // 측정 시작 시간 + +// ===== 함수 선언 ===== +void performCalibration(); +void processCommand(char cmd); +void printHelp(); + +void setup() { + Serial.begin(9600); + + // 시작 메시지 + Serial.println("================================="); + Serial.println(" LoadCellMeasure 시작"); + Serial.println("================================="); + + // HX711 초기화 + scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN); + + Serial.println("HX711 초기화 중..."); + delay(500); + + // HX711 연결 확인 + if (scale.is_ready()) { + Serial.println("HX711 연결 성공!"); + } else { + Serial.println("HX711 연결 실패! 배선을 확인하세요."); + Serial.println("DT 핀: " + String(LOADCELL_DOUT_PIN)); + Serial.println("SCK 핀: " + String(LOADCELL_SCK_PIN)); + while(1); // 무한 대기 + } + + // 캘리브레이션 팩터 설정 + scale.set_scale(calibration_factor); + + // ===== 최초 1회 자동 캘리브레이션 ===== + performCalibration(); + + // 사용법 출력 + printHelp(); + + delay(1000); +} + +void loop() { + // 시리얼 명령어 처리 + if (Serial.available()) { + char cmd = Serial.read(); + processCommand(cmd); + } + + // 일시정지 상태면 측정 건너뛰기 + if (isPaused) { + delay(100); + return; + } + + // 로드셀 측정 + if (scale.is_ready()) { + // 무게 측정 (1회) - 실시간 출력 + float weight = abs(scale.get_units(1)); + + // 경과 시간 계산 (초 단위, 0.01초 정밀도) + float elapsedTime = (millis() - startTime) / 1000.0; + + // 결과 출력 (JSON 형식) + Serial.print("{\"weight\":"); + Serial.print(weight, 1); // 소수점 1자리 + Serial.print(",\"unit\":\"g\",\"time\":"); + Serial.print(elapsedTime, 2); // 소수점 2자리 (0.01초 단위) + Serial.print(",\"delay\":"); + Serial.print(measureDelay); + Serial.println("}"); + } + + delay(measureDelay); +} + +// ===== 최초 캘리브레이션 수행 ===== +void performCalibration() { + if (isCalibrated) { + return; // 이미 캘리브레이션 완료 + } + + Serial.println(); + Serial.println("---------------------------------"); + Serial.println(">> 최초 캘리브레이션 진행"); + Serial.println("---------------------------------"); + Serial.println("로드셀 위에 아무것도 올리지 마세요!"); + Serial.println("3초 후 영점 조정을 시작합니다..."); + delay(3000); + + Serial.println("영점 조정 중..."); + scale.tare(); // 영점 조정 + + Serial.println(">> 캘리브레이션 완료!"); + Serial.println("현재 Offset: " + String(scale.get_offset())); + Serial.println("---------------------------------"); + Serial.println(); + + isCalibrated = true; + startTime = millis(); // 측정 시작 시간 기록 +} + +// ===== 시리얼 명령어 처리 ===== +void processCommand(char cmd) { + cmd = tolower(cmd); + + switch(cmd) { + // 'c' 또는 'C' : 영점 조절 + case 'c': + Serial.println("\n>> 영점 조정 중..."); + Serial.println(" 로드셀 위에 아무것도 올리지 마세요!"); + delay(1000); + scale.tare(); // 영점 조정 + Serial.println(">> 영점 조정 완료!\n"); + break; + + // 'p' 또는 'P' : 일시정지 + case 'p': + if (!isPaused) { + isPaused = true; + Serial.println("\n>> 측정 일시정지됨"); + Serial.println(" 'r'을 입력하면 다시 시작합니다.\n"); + } else { + Serial.println("\n>> 이미 일시정지 상태입니다.\n"); + } + break; + + // 'r' 또는 'R' : 재시작 + case 'r': + if (isPaused) { + isPaused = false; + Serial.println("\n>> 측정 재시작!\n"); + } else { + Serial.println("\n>> 이미 측정 중입니다.\n"); + } + break; + + // '+' : 측정 속도 빠르게 (딜레이 감소) + case '+': + if (measureDelay > MIN_DELAY) { + measureDelay -= DELAY_STEP; + if (measureDelay < MIN_DELAY) measureDelay = MIN_DELAY; + Serial.println(">> 측정 간격 감소: " + String(measureDelay) + " ms (빠름)"); + } else { + Serial.println(">> 최소 측정 간격입니다: " + String(MIN_DELAY) + " ms"); + } + break; + + // '-' : 측정 속도 느리게 (딜레이 증가) + case '-': + if (measureDelay < MAX_DELAY) { + measureDelay += DELAY_STEP; + if (measureDelay > MAX_DELAY) measureDelay = MAX_DELAY; + Serial.println(">> 측정 간격 증가: " + String(measureDelay) + " ms (느림)"); + } else { + Serial.println(">> 최대 측정 간격입니다: " + String(MAX_DELAY) + " ms"); + } + break; + + // 'h' 또는 'H' : 도움말 + case 'h': + printHelp(); + break; + + // 그 외 문자는 무시 (줄바꿈, 공백 등) + default: + if (cmd != '\n' && cmd != '\r' && cmd != ' ') { + Serial.println(">> 알 수 없는 명령어: " + String(cmd)); + Serial.println(" 'h'를 입력하면 도움말을 볼 수 있습니다."); + } + break; + } +} + +// ===== 도움말 출력 ===== +void printHelp() { + Serial.println(); + Serial.println("---------------------------------"); + Serial.println("시리얼 모니터 명령어:"); + Serial.println(" 'c' : 영점 조절 (Calibration)"); + Serial.println(" 'p' : 일시정지 (Pause)"); + Serial.println(" 'r' : 재시작 (Resume)"); + Serial.println(" '+' : 측정 속도 빠르게"); + Serial.println(" '-' : 측정 속도 느리게"); + Serial.println(" 'h' : 도움말 보기"); + Serial.println("---------------------------------"); + Serial.println(); +} diff --git a/web_UI/README.md b/web_UI/README.md new file mode 100644 index 0000000..48b220a --- /dev/null +++ b/web_UI/README.md @@ -0,0 +1,179 @@ +# LoadCell 실시간 무게 측정 웹 GUI + +Arduino HX711 로드셀 데이터를 실시간으로 웹 브라우저에 그래프로 표시하는 애플리케이션입니다. + +## 시스템 구조 + +``` +[Arduino + HX711] --시리얼--> [Python 서버] --WebSocket--> [웹 브라우저] + (Flask + pyserial) (Chart.js) +``` + +## 기능 + +- 실시간 무게 데이터 그래프 표시 +- 현재 무게, 최대값, 평균값 통계 +- 영점 조절, 일시정지, 재시작 원격 제어 +- 시스템 로그 표시 +- 반응형 웹 디자인 + +## 설치 방법 + +### 1. Python 환경 설정 + +Python 3.7 이상이 필요합니다. + +```bash +# 가상환경 생성 (선택사항, 권장) +python -m venv venv + +# 가상환경 활성화 +# Windows: +venv\Scripts\activate +# macOS/Linux: +source venv/bin/activate + +# 필요한 패키지 설치 +pip install -r requirements.txt +``` + +### 2. Arduino 준비 + +1. Arduino IDE에서 `LoadCellMeasure.ino` 파일을 Arduino에 업로드 +2. Arduino와 PC가 USB로 연결되어 있는지 확인 +3. Arduino IDE의 시리얼 모니터는 **닫아야 합니다** (포트 충돌 방지) + +## 실행 방법 + +### 1. Python 서버 실행 + +```bash +cd web_gui +python app.py +``` + +서버가 시작되면 다음과 같은 메시지가 표시됩니다: + +``` +Arduino 포트 발견: COM3 +시리얼 포트 연결 성공! + +================================= +웹 서버 시작: http://localhost:5000 +================================= +``` + +### 2. 웹 브라우저로 접속 + +브라우저에서 다음 주소로 접속: + +``` +http://localhost:5000 +``` + +또는 같은 네트워크의 다른 기기에서: + +``` +http://[서버PC의IP주소]:5000 +``` + +## 사용 방법 + +### 웹 인터페이스 기능 + +1. **실시간 그래프**: 최근 50개의 무게 데이터를 실시간으로 표시 +2. **현재 무게**: 현재 측정 중인 무게 값 +3. **통계**: 최대값과 평균값 자동 계산 +4. **제어 버튼**: + - **영점 조절**: 로드셀 위의 물건을 치우고 영점 재설정 + - **일시정지**: 측정 일시 중지 + - **재시작**: 측정 재개 + - **그래프 초기화**: 그래프와 통계 데이터 초기화 +5. **시스템 로그**: Arduino와 서버의 메시지 표시 + +### Arduino 시리얼 명령어 + +웹 인터페이스 대신 Arduino IDE 시리얼 모니터에서도 제어 가능: + +- `c`: 영점 조절 +- `p`: 일시정지 +- `r`: 재시작 +- `+`: 측정 속도 빠르게 +- `-`: 측정 속도 느리게 +- `h`: 도움말 + +## 프로젝트 구조 + +``` +web_gui/ +├── app.py # Flask 백엔드 서버 +├── requirements.txt # Python 패키지 목록 +├── templates/ +│ └── index.html # 웹 프론트엔드 +└── README.md # 이 파일 +``` + +## 문제 해결 + +### Arduino 포트를 찾을 수 없습니다 + +1. Arduino가 USB로 연결되어 있는지 확인 +2. Arduino IDE에서 올바른 포트가 선택되어 있는지 확인 +3. 다른 프로그램(Arduino IDE 시리얼 모니터 등)이 포트를 사용 중인지 확인 +4. `app.py` 를 수정하여 포트를 수동으로 지정: + +```python +# app.py 마지막 부분 수정 +port = "COM3" # 또는 /dev/ttyUSB0 (Linux), /dev/cu.usbserial (macOS) +``` + +### 웹 페이지가 열리지 않습니다 + +1. Python 서버가 정상적으로 실행 중인지 확인 +2. 방화벽이 5000 포트를 차단하고 있는지 확인 +3. 브라우저 캐시를 삭제하고 새로고침 (`Ctrl+F5`) + +### 데이터가 표시되지 않습니다 + +1. Arduino 시리얼 모니터가 닫혀있는지 확인 (포트 충돌) +2. Arduino가 제대로 작동하는지 Arduino IDE 시리얼 모니터로 먼저 확인 +3. 브라우저 개발자 도구(F12)의 Console 탭에서 에러 확인 + +## 기술 스택 + +### 백엔드 +- **Flask**: 웹 서버 프레임워크 +- **pyserial**: Arduino 시리얼 통신 +- **Flask-SocketIO**: 실시간 WebSocket 통신 +- **eventlet**: 비동기 서버 처리 + +### 프론트엔드 +- **Chart.js**: 실시간 그래프 라이브러리 +- **Socket.IO**: 실시간 양방향 통신 +- **HTML/CSS/JavaScript**: 웹 인터페이스 + +## 커스터마이징 + +### 그래프 데이터 포인트 수 변경 + +`templates/index.html` 파일에서: + +```javascript +const maxDataPoints = 50; // 원하는 숫자로 변경 +``` + +### 측정 간격 조절 + +Arduino 코드의 `measureDelay` 변수 조절 또는 웹에서 `+`/`-` 키로 조절 + +### 서버 포트 변경 + +`app.py` 마지막 부분: + +```python +socketio.run(app, host='0.0.0.0', port=5000, debug=False) # 포트 번호 변경 +``` + +## 라이선스 + +이 프로젝트는 교육 및 연구 목적으로 자유롭게 사용할 수 있습니다. diff --git a/web_UI/app.py b/web_UI/app.py new file mode 100644 index 0000000..35d91af --- /dev/null +++ b/web_UI/app.py @@ -0,0 +1,180 @@ +""" +LoadCell Weight Monitor - 실시간 무게 측정 웹 GUI +Arduino에서 시리얼로 JSON 데이터를 받아 웹 브라우저에 실시간 그래프로 표시 +""" + +from flask import Flask, render_template +from flask_socketio import SocketIO +import serial +import serial.tools.list_ports +import json +import threading +import time +import logging + +# Werkzeug HTTP 요청 로그 비활성화 (Socket.IO polling 로그 숨김) +logging.getLogger('werkzeug').setLevel(logging.ERROR) + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'loadcell_secret_2025' +socketio = SocketIO(app, cors_allowed_origins="*") + +# 전역 변수 +serial_port = None +serial_thread = None +is_running = False + +# ===== Arduino 포트 자동 검색 ===== +def find_arduino_port(): + """사용 가능한 Arduino 포트를 자동으로 찾습니다""" + ports = serial.tools.list_ports.comports() + for port in ports: + # Arduino가 연결된 포트 찾기 (USB Serial, CH340, Arduino 등) + if 'USB' in port.description or 'Arduino' in port.description or 'CH340' in port.description: + return port.device + + # 못 찾으면 사용 가능한 포트 목록 출력 + if ports: + print("\n사용 가능한 포트:") + for port in ports: + print(f" - {port.device}: {port.description}") + return ports[0].device # 첫 번째 포트 반환 + return None + +# ===== 시리얼 데이터 읽기 스레드 ===== +def read_serial_data(): + """Arduino로부터 시리얼 데이터를 읽어 WebSocket으로 전송""" + global is_running, serial_port + + error_count = 0 + max_errors = 5 # 연속 에러 허용 횟수 + + while is_running: + try: + if serial_port and serial_port.is_open: + # in_waiting 호출을 안전하게 처리 + try: + waiting = serial_port.in_waiting + except Exception: + waiting = 0 + + if waiting > 0: + line = serial_port.readline().decode('utf-8', errors='ignore').strip() + + # JSON 데이터 파싱 + if line.startswith('{') and line.endswith('}'): + try: + data = json.loads(line) + socketio.emit('weight_data', data) + print(f"Data: {data}") + error_count = 0 # 성공 시 에러 카운트 리셋 + except json.JSONDecodeError: + print(f"Invalid JSON: {line}") + elif line: # 빈 줄이 아니면 + socketio.emit('log_message', {'message': line}) + print(f"Log: {line}") + + time.sleep(0.02) # CPU 사용률 낮추기 + + except Exception as e: + error_count += 1 + print(f"Serial error ({error_count}/{max_errors}): {e}") + + if error_count >= max_errors: + print("연속 에러 발생. 포트 재연결 시도...") + socketio.emit('log_message', {'message': '시리얼 재연결 시도 중...'}) + + # 포트 재연결 시도 + try: + if serial_port: + serial_port.close() + time.sleep(2) + + port = find_arduino_port() + if port: + serial_port = serial.Serial(port, 9600, timeout=1) + time.sleep(10) + print(f"포트 재연결 성공: {port}") + socketio.emit('log_message', {'message': f'재연결 성공: {port}'}) + else: + print("Arduino 포트를 찾을 수 없습니다.") + socketio.emit('log_message', {'message': 'Arduino 연결 실패'}) + except Exception as reconnect_error: + print(f"재연결 실패: {reconnect_error}") + + error_count = 0 # 재연결 시도 후 카운트 리셋 + + time.sleep(1) + +# ===== Flask 라우트 ===== +@app.route('/') +def loadcell_measure(): + """메인 페이지""" + return render_template('loadcell_measure.html') + +# ===== SocketIO 이벤트 ===== +@socketio.on('connect') +def handle_connect(): + """클라이언트 연결""" + print('Client connected') + socketio.emit('log_message', {'message': 'Connected to server'}) + +@socketio.on('disconnect') +def handle_disconnect(): + """클라이언트 연결 해제""" + print('Client disconnected') + +@socketio.on('send_command') +def handle_command(data): + """클라이언트에서 Arduino로 명령 전송""" + global serial_port + command = data.get('command', '') + + if serial_port and serial_port.is_open: + try: + serial_port.write(command.encode('utf-8')) + print(f"Sent command: {command}") + socketio.emit('log_message', {'message': f'Command sent: {command}'}) + except Exception as e: + print(f"Error sending command: {e}") + socketio.emit('log_message', {'message': f'Error: {e}'}) + else: + socketio.emit('log_message', {'message': 'Serial port not connected'}) + +# ===== 메인 실행 ===== +if __name__ == '__main__': + # Arduino 포트 찾기 + port = find_arduino_port() + + if port: + print(f"\nArduino 포트 발견: {port}") + try: + # 시리얼 포트 열기 + serial_port = serial.Serial(port, 9600, timeout=1) + time.sleep(5) # Arduino 리셋 대기 + print("시리얼 포트 연결 성공!") + + # 시리얼 읽기 스레드 시작 + is_running = True + serial_thread = threading.Thread(target=read_serial_data, daemon=True) + serial_thread.start() + + # Flask 서버 시작 + print("\n=================================") + print("웹 서버 시작: http://localhost:5000") + print("=================================\n") + socketio.run(app, host='0.0.0.0', port=5000, debug=False) + + except serial.SerialException as e: + print(f"시리얼 포트 오류: {e}") + print("Arduino가 제대로 연결되어 있는지 확인하세요.") + except KeyboardInterrupt: + print("\n서버 종료 중...") + finally: + is_running = False + if serial_port and serial_port.is_open: + serial_port.close() + print("서버 종료 완료") + else: + print("Arduino 포트를 찾을 수 없습니다!") + print("Arduino가 연결되어 있는지 확인하세요.") diff --git a/web_UI/requirements.txt b/web_UI/requirements.txt new file mode 100644 index 0000000..70b6d07 --- /dev/null +++ b/web_UI/requirements.txt @@ -0,0 +1,5 @@ +Flask==3.0.0 +pyserial==3.5 +flask-socketio==5.3.5 +python-socketio==5.10.0 +eventlet==0.33.3 diff --git a/web_UI/templates/loadcell_measure.html b/web_UI/templates/loadcell_measure.html new file mode 100644 index 0000000..ea608c8 --- /dev/null +++ b/web_UI/templates/loadcell_measure.html @@ -0,0 +1,459 @@ + + + + + + LoadCell 실시간 무게 측정 + + + + + + + + + + +
+ +
+

LoadCell 실시간 무게 측정

+
연결 대기 중...
+
+ + +
+ +
+

실시간 무게 그래프

+
+ +
+
+ + +
+

측정 정보

+ + +
+
현재 무게
+
0.0
+
g
+
+ + +
+
+
최대값
+
0.0
+
+
+
평균값
+
0.0
+
+
+
측정 시간
+
0.00
+
+
+
측정 간격
+
10ms
+
+
+ + +
+

측정 속도 조절

+
+ +
+ 느리게 / 빠르게 +
+ +
+
+ + +
+ + + + +
+
+
+ + +
+

시스템 로그

+
+
+
+ + + +