From bc2da5dd7f85279d63503a2529b222c6e598b6ed Mon Sep 17 00:00:00 2001 From: shkim Date: Thu, 15 Jan 2026 16:49:09 +0900 Subject: [PATCH] =?UTF-8?q?feat:=EB=A1=9C=EB=93=9C=EC=85=80=203=EA=B0=9C?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=EB=A1=9C=EC=A7=81=20-=20=EB=9D=BC?= =?UTF-8?q?=EC=A6=88=EB=B2=A0=EB=A6=AC=ED=8C=8C=EC=9D=B4=20->=20=EC=95=84?= =?UTF-8?q?=EB=91=90=EC=9D=B4=EB=85=B8=20=EA=B5=AC=EB=8F=99=EC=9D=84=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?-=20=ED=95=98=EB=93=9C=EC=9B=A8=EC=96=B4=20=EA=B5=AC=EC=84=B1?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EA=B8=B0=EB=B3=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=AC=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/measure.cpython-313.pyc | Bin 0 -> 10785 bytes main.py | 97 ++++++++++ measure.py | 286 ++++++++++++++++++++++++++++ web.html | 63 ++++++ 4 files changed, 446 insertions(+) create mode 100644 __pycache__/measure.cpython-313.pyc create mode 100644 main.py create mode 100644 measure.py create mode 100644 web.html diff --git a/__pycache__/measure.cpython-313.pyc b/__pycache__/measure.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..40087d4224e410332f3cc573bf7f05af7781213d GIT binary patch literal 10785 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 literal 0 HcmV?d00001 diff --git a/main.py b/main.py new file mode 100644 index 0000000..b04b7b1 --- /dev/null +++ b/main.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +""" +main.py - 오케스트레이션 파일 +measure.py와 웹서버를 함께 실행 +""" + +from flask import Flask, jsonify, send_file +import threading +import time + +# measure.py에서 필요한 것들 import +from measure import init_hx711, perform_calibration, get_weight + +app = Flask(__name__) + +# 최신 측정 데이터 저장 +latest_data = { + "volume_ml": 0, + "weight_avg": 0, + "w1": 0, + "w2": 0, + "w3": 0, + "time": 0 +} +start_time = 0 + + +@app.route('/') +def index(): + """웹 페이지 서빙""" + return send_file('web.html') + + +@app.route('/data') +def data(): + """측정 데이터 JSON 반환""" + return jsonify(latest_data) + + +def measure_loop(): + """측정 루프 (백그라운드 스레드)""" + global latest_data, start_time + + print("측정 시작...") + start_time = time.time() + + while True: + try: + weight_avg, volume_ml, w1, w2, w3 = get_weight() + elapsed = time.time() - start_time + + latest_data = { + "volume_ml": volume_ml, + "weight_avg": weight_avg, + "w1": w1, + "w2": w2, + "w3": w3, + "time": elapsed + } + + # 콘솔에도 출력 + print(f"Volume: {volume_ml:.1f}mL | W1:{w1:.1f} W2:{w2:.1f} W3:{w3:.1f}") + + except Exception as e: + print(f"측정 오류: {e}") + + time.sleep(0.1) + + +def main(): + print("=" * 50) + print(" Load Cell Web Monitor") + print("=" * 50) + + # 1. HX711 초기화 + if not init_hx711(): + print("HX711 초기화 실패!") + return + + # 2. 캘리브레이션 + perform_calibration() + + # 3. 측정 스레드 시작 + measure_thread = threading.Thread(target=measure_loop, daemon=True) + measure_thread.start() + + # 4. 웹서버 시작 + print("\n" + "=" * 50) + print(" 웹서버 시작: http://localhost:5000") + print(" Ctrl+C로 종료") + print("=" * 50 + "\n") + + app.run(host='0.0.0.0', port=5000, debug=False) + + +if __name__ == "__main__": + main() diff --git a/measure.py b/measure.py new file mode 100644 index 0000000..6e42293 --- /dev/null +++ b/measure.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +""" +LoadCell - Raspberry Pi Python library for HX711 load cell amplifier + +기능 : +1. 무게 측정 (양수 값으로 출력) +2. 키보드 명령어: 'c' 영점조절, 'p' 일시정지, 'r' 재시작 +3. 시작 시 자동 캘리브레이션 + +하드웨어 연결: +- HX711 [VCC] -> Pi 5 [Pin 1] (3.3V) +- HX711 [GND] -> Pi 5 [Pin 6] (GND) +- HX711 [DT] -> Pi 5 [Pin 29] (GPIO5) +- HX711 [SCK] -> Pi 5 [Pin 31] (GPIO6) +""" + +from HX711 import SimpleHX711, Rate +import time +import sys +import select +import tty +import termios + +# HX711 #1 핀 설정 +DT_PIN = 5 # GPIO5 -> 29번 핀 +SCK_PIN = 6 # GPIO6 -> 31번 핀 + +# HX711 #2 핀 설정 +DT_PIN_2 = 17 # GPIO17 -> 11번 핀 +SCK_PIN_2 = 27 # GPIO27 -> 13번 핀 + +# HX711 #3 핀 설정 +DT_PIN_3 = 22 # GPIO22 -> 15번 핀 +SCK_PIN_3 = 23 # GPIO23 -> 16번 핀 + +# 설정 변수 +CALIBRATION_FACTOR_1 = -411.17 # 캘리브레이션 팩터 #1 +CALIBRATION_FACTOR_2 = -409.85 # 캘리브레이션 팩터 #2 +CALIBRATION_FACTOR_3 = -410.20 # 캘리브레이션 팩터 #3 + +measure_delay = 0.1 # 측정 간격 (초) - 기본 100ms +DELAY_STEP = 0.01 # 측정 간격 조절 단위 (10ms) +density = 1.0 # 기본값: 물 (1.0 g/ml) + +# 상태 변수 +is_paused = False +is_calibrated = False +start_time = 0 +offset_value_1 = 0 +offset_value_2 = 0 +offset_value_3 = 0 + + + +# HX711 인스턴스 +hx1 = None +hx2 = None +hx3 = None + + +def init_hx711(): + """HX711 초기화""" + global hx1, hx2, hx3 + + print("HX711 초기화 중...") + + try: + hx1 = SimpleHX711(DT_PIN, SCK_PIN, 1, 0, Rate.HZ_10) + print("HX711 #1 연결 성공!") + + hx2 = SimpleHX711(DT_PIN_2, SCK_PIN_2, 1, 0, Rate.HZ_10) + print("HX711 #2 연결 성공!") + + hx3 = SimpleHX711(DT_PIN_3, SCK_PIN_3, 1, 0, Rate.HZ_10) + print("HX711 #3 연결 성공!") + return True + + except Exception as e: + print(f"HX711 연결 실패: {e}") + print("배선 확인 필요") + return False + + +def perform_calibration(): + """캘리브레이션 수행""" + global is_calibrated, offset_value_1, offset_value_2, offset_value_3, start_time + + if is_calibrated: + return + + print() + print("-" * 50) + print(">> 최초 캘리브레이션 진행") + print("-" * 50) + print("로드셀 위에 아무것도 올리지 마세요!") + print("2초 후 영점 조정을 시작합니다...") + time.sleep(2) + + print("영점 조정 중...") + + readings_1 = [] + readings_2 = [] + readings_3 = [] + + for _ in range(10): + readings_1.append(hx1.read()) + time.sleep(0.1) + offset_value_1 = sum(readings_1) / len(readings_1) + + + for _ in range(10): + readings_2.append(hx2.read()) + time.sleep(0.1) + offset_value_2 = sum(readings_2) / len(readings_2) + + for _ in range(10): + readings_3.append(hx3.read()) + time.sleep(0.1) + offset_value_3 = sum(readings_3) / len(readings_3) + + print(">> 캘리브레이션 완료!") + print(f"현재 오프셋 값: {offset_value_1:.2f}, {offset_value_2:.2f}, {offset_value_3:.2f}") + print() + + is_calibrated = True + start_time = time.time() + +def tare(): + """영점 조정 함수""" + global offset_value_1, offset_value_2, offset_value_3 + + print("\n>> 영점 조정 중...") + print(" 로드셀 위에 아무것도 올리지 마세요") + time.sleep(1) + + readings_1 = [] + for _ in range(10): + readings_1.append(hx1.read()) + time.sleep(0.1) + + offset_value_1 = sum(readings_1) / len(readings_1) + + readings_2 = [] + for _ in range(10): + readings_2.append(hx2.read()) + time.sleep(0.1) + + offset_value_2 = sum(readings_2) / len(readings_2) + + + readings_3 = [] + for _ in range(10): + readings_3.append(hx3.read()) + time.sleep(0.1) + + offset_value_3 = sum(readings_3) / len(readings_3) + print(">> 영점 조정 완료!\n") + +def get_weight(): + """무게 측정 함수""" + raw_value_1 = hx1.read() + raw_value_2 = hx2.read() + raw_value_3 = hx3.read() + + weight_g_1 = (raw_value_1 - offset_value_1) / CALIBRATION_FACTOR_1 + weight_g_2 = (raw_value_2 - offset_value_2) / CALIBRATION_FACTOR_2 + weight_g_3 = (raw_value_3 - offset_value_3) / CALIBRATION_FACTOR_3 + + weight_avg = (weight_g_1 + weight_g_2 + weight_g_3) / 3 + + volume_ml = (weight_avg / density)*3 + + return weight_avg, volume_ml, weight_g_1, weight_g_2, weight_g_3 + +def print_help(): + """도움말 출력""" + print() + print("-" * 50) + print("키보드 명령어:") + print(" 'c' : 영점 조절 (Calibration)") + print(" 'p' : 일시정지 (Pause)") + print(" 'r' : 재시작 (Resume)") + print(" 'h' : 도움말 보기") + print(" 'q' : 종료 (Quit)") + print("-" * 50) + print() + +def check_input(): + """비동기 키보드 입력 확인""" + if select.select([sys.stdin], [], [], 0)[0]: + return sys.stdin.read(1) + return None + +def process_command(cmd): + """명령어 처리""" + global is_paused + + cmd = cmd.lower() + + if cmd == 'c': + tare() + + elif cmd == 'p': + if not is_paused: + is_paused = True + print("\n>> 측정 일시정지됨") + print(" 'r'을 입력하면 다시 시작합니다.\n") + else: + print("\n>> 이미 일시정지 상태입니다.\n") + + elif cmd == 'r': + if is_paused: + is_paused = False + print("\n>> 측정 재시작!\n") + else: + print("\n>> 이미 측정 중입니다.\n") + + elif cmd == 'h': + print_help() + + elif cmd == 'q': + print("\n>> 프로그램 종료\n") + return False + + elif cmd not in ['\n', '\r', ' ']: + print(f">> 알 수 없는 명령어: {cmd}") + print(" 'h'를 입력하면 도움말을 볼 수 있습니다.") + + return True + +def set_paused(value): + global is_paused + is_paused = value + +def set_density(value): + global density + density = value + + +def main(): + global start_time + + if not init_hx711(): + return + + perform_calibration() + print_help() + + old_settings = termios.tcgetattr(sys.stdin) + + try: + tty.setcbreak(sys.stdin.fileno()) + print("측정 시작 (종료하려면 'q' 입력)...\n") + + while True: + cmd = check_input() + if cmd: + if not process_command(cmd): + break + + if is_paused: + time.sleep(0.1) + continue + + weight_avg, volume_ml, weight_g_1, weight_g_2, weight_g_3 = get_weight() + elapsed_time = time.time() - start_time + + # JSON 형식으로 출력 + print(f'{{"volume_ml":{volume_ml:.1f},"w1":{weight_g_1:.1f},"w2":{weight_g_2:.1f},"w3":{weight_g_3:.1f},"time":{elapsed_time:.2f}}}') + print(f"─────────────────────") + print(f" volume_ml : {volume_ml:.1f} mL") + print(f" w1 : {weight_g_1:.1f} g") + print(f" w2 : {weight_g_2:.1f} g") + print(f" w3 : {weight_g_3:.1f} g") + print(f" time : {elapsed_time:.2f} s") + time.sleep(measure_delay) + + except KeyboardInterrupt: + print("\n>> 프로그램 종료\n") + + finally: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) + print("프로그램 종료됨.") + +if __name__ == "__main__": + main() diff --git a/web.html b/web.html new file mode 100644 index 0000000..08ed267 --- /dev/null +++ b/web.html @@ -0,0 +1,63 @@ + + + + + Load Cell Monitor + + + +

Load Cell Monitor

+ +
+
Volume (mL)
+
--
+
+ +
+
Weight Average (g)
+
--
+
+ +
+
Individual Weights (g)
+
+ -- | + -- | + -- +
+
+ +
Time: --s
+ + + +