test commit

This commit is contained in:
2026-01-14 14:05:24 +09:00
parent 8b4edbbfe6
commit e5d1c98cfa
9 changed files with 922 additions and 21 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

153
apiserver.py Normal file
View File

@@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""
FASTAPI + Socket.IO 서버 - LoadCell 실시간 데이터 중계
센서 데이터를 웹에 실시간 전달 (Socket.IO)
"""
import os
import socketio
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
# ===== Socket.IO 서버 생성 =====
sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
# ===== FastAPI 인스턴스 생성 =====
fastapi_app = FastAPI(
title="LoadCell API Server",
description="HX711 로드셀 측정 데이터 API + Socket.IO",
version="2.0.0",
)
# Socket.IO ASGI 앱 (FastAPI와 결합)
app = socketio.ASGIApp(sio, fastapi_app)
# 정적 파일 서빙
if os.path.exists("static"):
fastapi_app.mount("/static", StaticFiles(directory="static"), name="static")
# 공유 데이터 (main.py에서 set_shared_data()로 설정)
shared_data = None
def set_shared_data(data: dict):
"""main.py에서 호출하여 측정 데이터 연결"""
global shared_data
shared_data = data
# ===== 요청 모델 정의 =====
class CommandRequest(BaseModel):
"""명령어 요청 모델"""
command: str # 'tare', 'pause', 'resume'
# ===== FastAPI 라우트 =====
@fastapi_app.get("/")
def index():
"""메인 페이지"""
return FileResponse("web.html")
@fastapi_app.get("/uro/data")
def get_data():
"""REST API로 현재 데이터 조회 (폴백용)"""
if shared_data:
return shared_data
return {"volume_ml": 0, "time": 0, "status": "대기 중"}
@fastapi_app.post("/uro/command")
def handle_command(req: CommandRequest):
"""REST API로 명령어 처리"""
import rapimeasure
return process_command(req.command)
def process_command(command: str):
"""명령어 처리 공통 함수"""
import rapimeasure
if command == "tare":
rapimeasure.tare()
return {"success": True, "message": "영점 조정 완료"}
elif command == "pause":
rapimeasure.set_paused(True)
if shared_data:
shared_data["status"] = "일시정지"
return {"success": True, "message": "측정 일시 정지"}
elif command == "resume":
rapimeasure.set_paused(False)
if shared_data:
shared_data["status"] = "측정 중"
return {"success": True, "message": "측정 재시작"}
return {"success": False, "message": f"알 수 없는 명령: {command}"}
# ===== Socket.IO 이벤트 핸들러 =====
@sio.event
async def connect(sid, environ):
"""클라이언트 연결"""
print(f"[Socket.IO] Client connected: {sid}")
await sio.emit("log_message", {"message": "서버에 연결되었습니다."}, to=sid)
@sio.event
async def disconnect(sid):
"""클라이언트 연결 해제"""
print(f"[Socket.IO] Client disconnected: {sid}")
@sio.event
async def send_command(sid, data):
"""클라이언트에서 명령어 수신"""
command = data.get("command", "")
print(f"[Socket.IO] Command from {sid}: {command}")
result = process_command(command)
await sio.emit("command_result", result, to=sid)
@sio.event
async def set_density(sid, data):
"""밀도 설정"""
import rapimeasure
density = data.get("density", 1.0)
rapimeasure.set_density(density)
print(f"[Socket.IO] Density set to: {density}")
await sio.emit("log_message", {"message": f"밀도 설정: {density} g/mL"}, to=sid)
@sio.event
async def reset_measurement(sid):
"""측정 초기화"""
print(f"[Socket.IO] Reset measurement from {sid}")
await sio.emit("log_message", {"message": "측정 데이터 초기화"}, to=sid)
# ===== 외부에서 호출할 emit 함수 =====
async def emit_weight_data(data: dict):
"""측정 데이터를 모든 클라이언트에게 브로드캐스트"""
# 디버깅: 데이터 전송 로그 출력 (너무 많으면 주석 처리)
# print(f"[Socket.IO] Emitting data: {data['weight_g']}g")
await sio.emit("weight_data", data)
# ===== 직접 실행 (테스트용) =====
if __name__ == "__main__":
import uvicorn
# 더미 데이터
shared_data = {"volume_ml": 0, "time": 0, "status": "테스트 모드"}
print("API Server Test Mode")
uvicorn.run(app, host="0.0.0.0", port=8080)

112
main.py Normal file
View File

@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""
메인 실행 파일
- 백그라운드 스레드: 센서 데이터 측정
- 메인 스레드(비동기): FastAPI + Socket.IO 서버 실행
"""
import asyncio
import threading
import time
import rapimeasure
import apiserver
import uvicorn
# 측정 데이터를 저장할 공유 dict 기본값
shared_data = {"volume_ml": 0
, "time": 0
, "status": "Initializing"
}
# 이벤트 루프 참조 (Socket.IO emit용)
main_loop = None
def measurement_loop():
"""
백그라운드 스레드에서 실행되는 측정 무한 루프
센서 데이터를 읽고 Socket.IO로 브로드캐스트
"""
global main_loop
print("[measure Thread] start")
# 센서 초기화
rapimeasure.init_hx711()
shared_data["status"] = "Measuring"
measurement_count = 0
while True:
try:
# 일시정지 체크
if rapimeasure.is_paused:
time.sleep(0.1)
continue
# 센서 데이터 읽기
_, volume_ml = rapimeasure.get_weight()
# 공유 데이터 업데이트
shared_data["volume_ml"] = round(volume_ml, 2)
shared_data["time"] = measurement_count
# Socket.IO로 실시간 브로드캐스트
if main_loop is not None:
try:
# Thread-safe하게 비동기 emit 함수 호출
asyncio.run_coroutine_threadsafe(
apiserver.emit_weight_data(shared_data.copy()), main_loop
)
except Exception:
pass # emit 실패 시 무시 (연결 종료 등)
measurement_count += 1
# 200ms 간격으로 측정 (5Hz)
time.sleep(0.2)
except Exception as e:
print(f"[measure error] {e}")
shared_data["status"] = f"error: {e}"
time.sleep(1)
async def main():
"""비동기 메인 함수"""
global main_loop
main_loop = asyncio.get_running_loop()
print("=" * 50)
print(" LoadCell Dashboard + Socket.IO")
print(" access address:")
print(" - local: http://localhost:8080")
print(" - network: http://192.168.0.191:8080")
print(" Swagger document: http://localhost:8080/docs")
print("=" * 50)
# 1. 공유 데이터를 API 서버에 연결
apiserver.set_shared_data(shared_data)
# 2. 측정 스레드 시작
measure_thread = threading.Thread(target=measurement_loop, daemon=True)
measure_thread.start()
print("[main] measure thread started")
print("[main] FastAPI + Socket.IO server starting...")
# 3. uvicorn 서버 실행
config = uvicorn.Config(
apiserver.app,
host="0.0.0.0",
port=8080,
log_level="warning", # info -> warning으로 줄여서 로그 깔끔하게
)
server = uvicorn.Server(config)
await server.serve()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nProgram terminated.")

View File

@@ -27,18 +27,27 @@ SCK_PIN = 6 # GPIO6 -> 31번 핀
# 설정 변수 # 설정 변수
CALIBRATION_FACTOR = -411.17 # 캘리브레이션 팩터 CALIBRATION_FACTOR = -411.17 # 캘리브레이션 팩터
measure_delay = 0.1 # 측정 간격 (초) measure_delay = 0.1 # 측정 간격 (초) - 기본 100ms
DELAY_STEP = 0.01 # 측정 간격 조절 단위 (10ms)
density = 1.0 # 기본값: 물 (1.0 g/ml) density = 1.0 # 기본값: 물 (1.0 g/ml)
# 상태 변수 # 상태 변수
is_paused = False is_paused = False
is_calibrated = False is_calibrated = False
start_time = 0
offset_value = 0 offset_value = 0
def set_paused(value):
global is_paused
is_paused = value
def set_density(value):
global density
density = value
# HX711 인스턴스 # HX711 인스턴스
hx = None hx = None
def init_hx711(): def init_hx711():
"""HX711 초기화""" """HX711 초기화"""
global hx global hx
@@ -55,10 +64,9 @@ def init_hx711():
print("배선 확인 필요") print("배선 확인 필요")
return False return False
def perform_calibration(): def perform_calibration():
"""캘리브레이션 수행""" """캘리브레이션 수행"""
global is_calibrated, offset_value global is_calibrated, offset_value, start_time
if is_calibrated: if is_calibrated:
return return
@@ -85,7 +93,7 @@ def perform_calibration():
print() print()
is_calibrated = True is_calibrated = True
start_time = time.time()
def tare(): def tare():
"""영점 조정 함수""" """영점 조정 함수"""
@@ -103,16 +111,14 @@ def tare():
offset_value = sum(readings) / len(readings) offset_value = sum(readings) / len(readings)
print(">> 영점 조정 완료!\n") print(">> 영점 조정 완료!\n")
def get_weight(): def get_weight():
"""무게 측정 함수""" """무게 측정 함수"""
raw_value = hx.read() raw_value = hx.read()
weight_g = (raw_value - offset_value) / CALIBRATION_FACTOR weight_g = (raw_value - offset_value) / CALIBRATION_FACTOR
weight_g = max(0, weight_g)
volume_ml = weight_g / density volume_ml = weight_g / density
return weight_g, volume_ml return weight_g, volume_ml
def print_help(): def print_help():
"""도움말 출력""" """도움말 출력"""
print() print()
@@ -126,14 +132,12 @@ def print_help():
print("-" * 50) print("-" * 50)
print() print()
def check_input(): def check_input():
"""비동기 키보드 입력 확인""" """비동기 키보드 입력 확인"""
if select.select([sys.stdin], [], [], 0)[0]: if select.select([sys.stdin], [], [], 0)[0]:
return sys.stdin.read(1) return sys.stdin.read(1)
return None return None
def process_command(cmd): def process_command(cmd):
"""명령어 처리""" """명령어 처리"""
global is_paused global is_paused
@@ -171,8 +175,9 @@ def process_command(cmd):
return True return True
def main(): def main():
global start_time
if not init_hx711(): if not init_hx711():
return return
@@ -183,7 +188,7 @@ def main():
try: try:
tty.setcbreak(sys.stdin.fileno()) tty.setcbreak(sys.stdin.fileno())
print("명령어 테스트 (q: 종료)...\n") print("측정 시작 (종료하려면 'q' 입력)...\n")
while True: while True:
cmd = check_input() cmd = check_input()
@@ -196,7 +201,10 @@ def main():
continue continue
weight_g, volume_ml = get_weight() weight_g, volume_ml = get_weight()
print(f"측정: {weight_g:.1f}g / {volume_ml:.1f}ml") elapsed_time = time.time() - start_time
# JSON 형식으로 출력
print(f'{{"volume_ml(mL)":{volume_ml:.1f},"density":{density},"time":{elapsed_time:.2f},"delay":{int(measure_delay*1000)}}}')
time.sleep(measure_delay) time.sleep(measure_delay)
except KeyboardInterrupt: except KeyboardInterrupt:
@@ -206,6 +214,5 @@ def main():
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
print("프로그램 종료됨.") print("프로그램 종료됨.")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

218
rapimeasure.py.save Normal file
View File

@@ -0,0 +1,218 @@
#!/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 핀 설정
DT_PIN = 5 # GPIO5 -> 29번 핀
SCK_PIN = 6 # GPIO6 -> 31번 핀
# 설정 변수
CALIBRATION_FACTOR = -411.17 # 캘리브레이션 팩터
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 = 0
def set_paused(value):
global is_paused
is_paused = value
def set_density(value):
global density
density = value
# HX711 인스턴스
hx = None
def init_hx711():
"""HX711 초기화"""
global hx
print("HX711 초기화 중...")
print(f" DT 핀: GPIO{DT_PIN} (Pin 29)")
print(f" SCK 핀: GPIO{SCK_PIN} (Pin 31)")
try:
hx = SimpleHX711(DT_PIN, SCK_PIN, 1, 0, Rate.HZ_10)
print("HX711 연결 성공!")
return True
except Exception as e:
print(f"HX711 연결 실패: {e}")
print("배선 확인 필요")
return False
def perform_calibration():
"""캘리브레이션 수행"""
global is_calibrated, offset_value, start_time
if is_calibrated:
return
print()
print("-" * 50)
print(">> 최초 캘리브레이션 진행")
print("-" * 50)
print("로드셀 위에 아무것도 올리지 마세요!")
print("2초 후 영점 조정을 시작합니다...")
time.sleep(2)
print("영점 조정 중...")
readings = []
for _ in range(10):
readings.append(hx.read())
time.sleep(0.1)
offset_value = sum(readings) / len(readings)
print(">> 캘리브레이션 완료!")
print(f"현재 오프셋 값: {offset_value:.2f}")
print()
is_calibrated = True
start_time = time.time()
def tare():
"""영점 조정 함수"""
global offset_value
print("\n>> 영점 조정 중...")
print(" 로드셀 위에 아무것도 올리지 마세요")
time.sleep(1)
readings = []
for _ in range(10):
readings.append(hx.read())
time.sleep(0.1)
offset_value = sum(readings) / len(readings)
print(">> 영점 조정 완료!\n")
def get_weight():
"""무게 측정 함수"""
raw_value = hx.read()
weight_g = (raw_value - offset_value) / CALIBRATION_FACTOR
weight_g = max(0, weight_g)
volume_ml = weight_g / density
return weight_g, volume_ml
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 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_g, volume_ml = get_weight()
elapsed_time = time.time() - start_time
# JSON 형식으로 출력
print(f'{{"volume_ml(mL)":{volume_ml:.1f},"density":{density},"time":{elapsed_time:.2f},"delay":{int(measure_delay*1000)}}}')
time.sleep(measure_delay)
except KeyboardInterrupt:
print("\n>> 프로그램 종료\n")
finally:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
print("프로그램 종료됨.")
if __name__ == "__main__":
main()

20
static/chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

391
web.html Normal file
View File

@@ -0,0 +1,391 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LoadCell 실시간 무게 측정</title>
<!-- Chart.js CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<!-- Socket.IO CDN -->
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.status {
display: inline-block;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.2);
border-radius: 20px;
font-size: 0.9em;
}
.status.connected {
background: rgba(76, 175, 80, 0.8);
}
.main-grid {
display: grid;
grid-template-columns: 1fr 350px;
gap: 20px;
margin-bottom: 20px;
}
.card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.chart-container {
position: relative;
height: 400px;
}
.current-weight {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 10px;
margin-bottom: 20px;
}
.current-weight .value {
font-size: 3.5em;
font-weight: bold;
margin: 10px 0;
}
.current-weight .unit {
font-size: 1.2em;
opacity: 0.9;
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 20px;
}
.btn {
padding: 12px 20px;
border: none;
border-radius: 8px;
font-size: 1em;
cursor: pointer;
transition: all 0.3s;
font-weight: 600;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-secondary {
background: #f0f0f0;
color: #333;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-success {
background: #4CAF50;
color: white;
}
.log-container {
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
height: 200px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 0.85em;
}
.log-item {
padding: 5px 0;
border-bottom: 1px solid #e0e0e0;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-top: 20px;
}
.stat-item {
text-align: center;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.stat-label {
font-size: 0.9em;
color: #666;
margin-bottom: 5px;
}
.stat-value {
font-size: 1.8em;
font-weight: bold;
color: #333;
}
@media (max-width: 1024px) {
.main-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>LoadCell 실시간 용량 측정</h1>
<div class="status" id="connectionStatus">연결 대기 중...</div>
</div>
<div class="main-grid">
<div class="card">
<h2 style="margin-bottom: 20px;">실시간 용량 그래프</h2>
<div class="chart-container">
<canvas id="weightChart"></canvas>
</div>
</div>
<div class="card">
<h2 style="margin-bottom: 20px;">측정 정보</h2>
<div class="density-input" style="margin-bottom: 15px;">
<label style="font-size: 0.9em; color: #666;">밀도 (g/mL):</label>
<input type="number" id="densityInput" value="1.0" min="0.1" max="10" step="0.01"
style="width: 80px; padding: 5px; border: 1px solid #ddd; border-radius: 4px; margin-left: 10px;">
<button class="btn btn-secondary" onclick="setDensity()"
style="padding: 5px 10px; margin-left: 5px;">적용</button>
</div>
<div class="current-weight"
style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); margin-top: 10px;">
<div class="label">현재 용량</div>
<div class="value" id="currentVolume">0.0</div>
<div class="unit">mL</div>
</div>
<h3 style="margin-top: 20px; margin-bottom: 10px; color: #333;">배뇨 측정 통계</h3>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-label">Voided Volume</div>
<div class="stat-value" id="voidedVolume">0.0<span style="font-size:0.5em">mL</span></div>
</div>
<div class="stat-item">
<div class="stat-label">Voided Time</div>
<div class="stat-value" id="voidedTime">0.0<span style="font-size:0.5em">s</span></div>
</div>
<div class="stat-item">
<div class="stat-label">Max Flow Rate</div>
<div class="stat-value" id="maxFlowRate">0.0<span style="font-size:0.5em">mL/s</span></div>
</div>
<div class="stat-item">
<div class="stat-label">Max Flow Time</div>
<div class="stat-value" id="maxFlowTime">0.0<span style="font-size:0.5em">s</span></div>
</div>
<div class="stat-item">
<div class="stat-label">Ave Flow Rate</div>
<div class="stat-value" id="aveFlowRate">0.0<span style="font-size:0.5em">mL/s</span></div>
</div>
</div>
<div class="controls">
<button class="btn btn-primary" onclick="sendCommand('tare')">영점 조절</button>
<button class="btn btn-secondary" onclick="sendCommand('pause')">일시정지</button>
<button class="btn btn-success" onclick="sendCommand('resume')">재시작</button>
<button class="btn btn-danger" onclick="resetMeasurement()">초기화</button>
</div>
</div>
</div>
<div class="card">
<h2 style="margin-bottom: 15px;">시스템 로그</h2>
<div class="log-container" id="logContainer"></div>
</div>
</div>
<script>
const socket = io();
const ctx = document.getElementById('weightChart').getContext('2d');
const maxDataPoints = 50;
const weightChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '용량 (mL)',
data: [],
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
borderWidth: 2,
tension: 0.4,
fill: true,
pointRadius: 3,
pointHoverRadius: 5
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 200 },
scales: {
x: { display: true, title: { display: true, text: '시간' } },
y: { beginAtZero: true, title: { display: true, text: '용량 (mL)' } }
}
}
});
let currentDensity = 1.0;
socket.on('connect', function () {
updateConnectionStatus(true);
addLog('서버에 연결되었습니다.');
});
socket.on('disconnect', function () {
updateConnectionStatus(false);
addLog('서버 연결이 끊어졌습니다.');
});
socket.on('weight_data', function (data) {
const volume = data.volume_ml || 0;
const timestamp = new Date().toLocaleTimeString();
weightChart.data.labels.push(timestamp);
weightChart.data.datasets[0].data.push(volume);
if (weightChart.data.labels.length > maxDataPoints) {
weightChart.data.labels.shift();
weightChart.data.datasets[0].data.shift();
}
weightChart.update('none');
document.getElementById('currentVolume').textContent = volume.toFixed(1);
});
socket.on('measurement_result', function (data) {
document.getElementById('voidedVolume').innerHTML = (data.voided_volume || 0).toFixed(1) + '<span style="font-size:0.5em">mL</span>';
document.getElementById('voidedTime').innerHTML = (data.voided_time || 0).toFixed(1) + '<span style="font-size:0.5em">s</span>';
document.getElementById('maxFlowRate').innerHTML = (data.max_flow_rate || 0).toFixed(2) + '<span style="font-size:0.5em">mL/s</span>';
document.getElementById('maxFlowTime').innerHTML = (data.max_flow_time || 0).toFixed(1) + '<span style="font-size:0.5em">s</span>';
if (data.ave_flow_rate !== undefined) {
document.getElementById('aveFlowRate').innerHTML = data.ave_flow_rate.toFixed(2) + '<span style="font-size:0.5em">mL/s</span>';
}
});
socket.on('log_message', function (data) { addLog(data.message); });
socket.on('command_result', function (data) {
addLog(data.success ? 'Okay: ' + data.message : 'Error: ' + data.message);
});
function updateConnectionStatus(connected) {
const statusEl = document.getElementById('connectionStatus');
if (connected) {
statusEl.textContent = '연결됨';
statusEl.classList.add('connected');
} else {
statusEl.textContent = '연결 끊김';
statusEl.classList.remove('connected');
}
}
function sendCommand(cmd) {
socket.emit('send_command', { command: cmd });
addLog('명령 전송: ' + cmd);
}
function setDensity() {
const densityInput = document.getElementById('densityInput');
const density = parseFloat(densityInput.value);
if (density > 0) {
currentDensity = density;
socket.emit('set_density', { density: density });
addLog('밀도 설정: ' + density + ' g/mL');
} else {
addLog('밀도는 0보다 커야 합니다.');
}
}
function resetMeasurement() {
weightChart.data.labels = [];
weightChart.data.datasets[0].data = [];
weightChart.update();
document.getElementById('currentVolume').textContent = '0.0';
document.getElementById('voidedVolume').innerHTML = '0.0<span style="font-size:0.5em">mL</span>';
document.getElementById('voidedTime').innerHTML = '0.0<span style="font-size:0.5em">s</span>';
document.getElementById('maxFlowRate').innerHTML = '0.0<span style="font-size:0.5em">mL/s</span>';
document.getElementById('maxFlowTime').innerHTML = '0.0<span style="font-size:0.5em">s</span>';
document.getElementById('aveFlowRate').innerHTML = '0.0<span style="font-size:0.5em">mL/s</span>';
socket.emit('reset_measurement');
sendCommand('tare');
addLog('측정 데이터 초기화');
}
function addLog(message) {
const logContainer = document.getElementById('logContainer');
const logItem = document.createElement('div');
logItem.className = 'log-item';
logItem.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logContainer.appendChild(logItem);
logContainer.scrollTop = logContainer.scrollHeight;
if (logContainer.children.length > 50) logContainer.removeChild(logContainer.firstChild);
}
</script>
</body>
</html>