Commit fcfb9aec authored by Sensor MVP Team's avatar Sensor MVP Team
Browse files

initial draft

parent 704ef42a
......@@ -177,3 +177,4 @@ sensor_data/
*.csv
*.dat
*.bin
JavaSDKV2.2.2/
# 센서 데이터 수집 시스템
리눅스 서버 기반 센서 데이터 수집 시스템을 Go 언어로 구현하여 기존 Java 기반 시스템을 대체하는 프로젝트입니다.
## 🏗️ 시스템 아키텍처
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 센서 디바이스 │────│ Java 브리지 │────│ Go 서버 │
│ │ │ (RSNet SDK) │ │ (REST API) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 웹 대시보드 │◄───│ PostgreSQL │◄───│ Redis 캐시 │
│ (React) │ │ (데이터 저장) │ │ (실시간 캐시) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
## 🚀 빠른 시작
### Docker Compose로 전체 시스템 실행
```bash
# 저장소 클론
git clone <repository-url>
cd docker_sensor_server
# Docker Compose로 모든 서비스 실행
docker-compose up -d
# 서비스 상태 확인
docker-compose ps
```
### 개별 서비스 접근
- **웹 대시보드**: http://localhost:3000
- **Go API 서버**: http://localhost:8080
- **PostgreSQL**: localhost:5432
- **Redis**: localhost:6379
- **Java 브리지**: localhost:8020
## 📁 프로젝트 구조
```
docker_sensor_server/
├── sensor-server/ # Go 메인 서버
│ ├── cmd/sensor-server/
│ ├── internal/
│ │ ├── api/ # REST API 핸들러
│ │ ├── cache/ # Redis 캐시
│ │ ├── database/ # PostgreSQL 연동
│ │ ├── models/ # 데이터 모델
│ │ ├── server/ # 서버 설정
│ │ └── websocket/ # WebSocket 처리
│ ├── Dockerfile
│ └── go.mod
├── sensor-bridge/ # Java 센서 브리지
│ ├── src/main/java/
│ ├── pom.xml
│ └── Dockerfile
├── web-dashboard/ # React 웹 대시보드
│ ├── src/
│ ├── package.json
│ └── Dockerfile
├── docker-compose.yml # 전체 시스템 설정
├── init-db.sql # 데이터베이스 초기화
└── README.md
```
## 🔧 API 엔드포인트
### 센서 데이터
- `POST /api/sensor-data` - 센서 데이터 수신
- `GET /api/devices/:deviceId/latest` - 최신 데이터 조회
- `GET /api/devices/:deviceId/history` - 히스토리 데이터 조회
### 디바이스 관리
- `GET /api/devices` - 디바이스 목록 조회
- `GET /api/health` - 헬스체크
### WebSocket
- `GET /ws` - 실시간 데이터 스트림
## 🛠️ 개발 환경 설정
### Go 서버 개발
```bash
cd sensor-server
# 의존성 설치
go mod tidy
# 개발 서버 실행
go run cmd/sensor-server/main.go
```
### Java 브리지 개발
```bash
cd sensor-bridge
# Maven 빌드
mvn clean package
# 실행
java -jar target/sensor-bridge-1.0.0.jar
```
### React 대시보드 개발
```bash
cd web-dashboard
# 의존성 설치
npm install
# 개발 서버 실행
npm start
```
## 📊 데이터 모델
### SensorReading
```go
type SensorReading struct {
ID uint `json:"id"`
DeviceID string `json:"device_id"`
NodeID int `json:"node_id"`
Temperature float64 `json:"temperature"`
Humidity float64 `json:"humidity"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
RecordedTime time.Time `json:"recorded_time"`
ReceivedTime time.Time `json:"received_time"`
CreatedAt time.Time `json:"created_at"`
}
```
### Device
```go
type Device struct {
ID uint `json:"id"`
DeviceID string `json:"device_id"`
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status"`
LastSeen time.Time `json:"last_seen"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
```
## 🔒 환경 변수
### Go 서버
```bash
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=password
DB_NAME=sensor_db
REDIS_HOST=localhost
REDIS_PORT=6379
PORT=8080
GIN_MODE=debug
```
### Java 브리지
```bash
GO_SERVER_URL=http://localhost:8080
RS_SERVER_PORT=8020
```
### React 대시보드
```bash
REACT_APP_API_URL=http://localhost:8080
REACT_APP_WS_URL=ws://localhost:8080
```
## 📈 모니터링
### 로그 확인
```bash
# Go 서버 로그
docker-compose logs sensor-server
# Java 브리지 로그
docker-compose logs sensor-bridge
# 데이터베이스 로그
docker-compose logs postgres
```
### 성능 메트릭
- 응답 시간: < 100ms
- 처리량: > 5,000 TPS
- 가용성: > 99.9%
- 메모리 사용량: < 1GB
## 🐛 문제 해결
### 일반적인 문제
1. **포트 충돌**
```bash
# 사용 중인 포트 확인
netstat -tulpn | grep :8080
# Docker Compose 재시작
docker-compose down && docker-compose up -d
```
2. **데이터베이스 연결 실패**
```bash
# PostgreSQL 컨테이너 상태 확인
docker-compose ps postgres
# 로그 확인
docker-compose logs postgres
```
3. **Java SDK 문제**
- `JavaSDKV2.2.2` 디렉토리가 올바른 위치에 있는지 확인
- JAR 파일이 올바르게 복사되었는지 확인
## 📝 라이선스
이 프로젝트는 MIT 라이선스 하에 배포됩니다.
## 🤝 기여
1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
\ No newline at end of file
# SENSOR MVP 프로젝트 인수인계서
## 📋 프로젝트 개요
**프로젝트명**: 센서 데이터 수집 시스템 MVP
**개발 기간**: 2024년 9월
**개발 언어**: Go, Java, React, TypeScript
**아키텍처**: 마이크로서비스 기반 Docker 컨테이너
**상태**: MVP 완성 및 운영 중
---
## 🏗️ 시스템 아키텍처
### 전체 시스템 구성도
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 센서 디바이스 │────│ Java 브리지 │────│ Go 서버 │
│ (RSNet SDK) │ │ (RSNet SDK) │ │ (REST API) │
│ │ │ (포트: 8020) │ │ (포트: 8080) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 웹 대시보드 │◄───│ PostgreSQL │◄───│ Redis 캐시 │
│ (React) │ │ (포트: 5432) │ │ (포트: 6379) │
│ (포트: 3000) │ │ (데이터 저장) │ │ (실시간 캐시) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
```
### 서비스 구성
| 서비스 | 기술 스택 | 포트 | 역할 | 상태 |
|--------|-----------|------|------|------|
| **sensor-server** | Go + Gin | 8080 | 메인 API 서버 | ✅ 운영 중 |
| **sensor-bridge** | Java + RSNet SDK | 8020 | 센서 데이터 브리지 | ✅ 운영 중 |
| **web-dashboard** | React + TypeScript | 3000 | 웹 대시보드 | ✅ 운영 중 |
| **postgres** | PostgreSQL 15 | 5432 | 메인 데이터베이스 | ✅ 운영 중 |
| **redis** | Redis 7 | 6379 | 실시간 캐시 | ✅ 운영 중 |
---
## 📁 프로젝트 구조 상세
### 루트 디렉토리
```
docker_sensor_server/
├── 📁 sensor-server/ # Go 메인 서버 (20MB 실행파일 포함)
├── 📁 sensor-bridge/ # Java 센서 브리지
├── 📁 web-dashboard/ # React 웹 대시보드
├── 📁 JavaSDKV2.2.2/ # RSNet SDK 라이브러리
├── 📁 init-db.sql/ # 데이터베이스 초기화 스크립트
├── 📄 docker-compose.yml # 전체 시스템 설정
├── 📄 deploy.sh # 배포 스크립트
├── 📄 backup.sh # 백업 스크립트
├── 📄 README.md # 프로젝트 문서
└── 📄 SENSOR 관련 인수인계서.hwp # 기존 인수인계서
```
### 1. Go 센서 서버 (sensor-server/)
```
sensor-server/
├── 📁 cmd/sensor-server/ # 메인 실행 파일
├── 📁 internal/ # 내부 패키지
│ ├── 📁 api/ # REST API 핸들러
│ ├── 📁 cache/ # Redis 캐시 연동
│ ├── 📁 database/ # PostgreSQL 연동
│ ├── 📁 models/ # 데이터 모델
│ ├── 📁 server/ # 서버 설정
│ └── 📁 websocket/ # WebSocket 처리
├── 📁 pkg/ # 공용 패키지
├── 📄 go.mod # Go 모듈 정의
├── 📄 go.sum # 의존성 체크섬
├── 📄 Dockerfile # Docker 이미지 빌드
└── 📄 env.example # 환경변수 예시
```
### 2. Java 센서 브리지 (sensor-bridge/)
```
sensor-bridge/
├── 📁 src/main/java/ # Java 소스 코드
│ └── 📁 com/sensor/bridge/ # 메인 패키지
│ ├── 📄 SensorBridge.java # 메인 클래스
│ ├── 📄 SensorDataParser.java # 센서 데이터 파싱
│ ├── 📄 TemperatureParser.java # 온도 파싱 시스템
│ ├── 📄 SensorParser.java # 센서 파서 인터페이스
│ ├── 📄 SHT30Parser.java # SHT30 센서 파서
│ ├── 📄 BME680Parser.java # BME680 센서 파서
│ ├── 📄 DefaultParser.java # 기본 파서
│ ├── 📄 SensorParserFactory.java # 파서 팩토리
│ ├── 📄 ParamItem.java # 파라미터 아이템
│ ├── 📄 SensorData.java # 센서 데이터 모델
│ ├── 📄 MemoryOptimizer.java # 메모리 최적화
│ ├── 📄 ErrorHandler.java # 에러 처리
│ └── 📄 LoggingSystem.java # 로깅 시스템
├── 📁 target/ # Maven 빌드 결과물
├── 📁 JavaSDKV2.2.2/ # RSNet SDK
├── 📄 pom.xml # Maven 프로젝트 설정
└── 📄 Dockerfile # Docker 이미지 빌드
```
### 3. React 웹 대시보드 (web-dashboard/)
```
web-dashboard/
├── 📁 src/ # React 소스 코드
├── 📁 public/ # 정적 파일
├── 📁 build/ # 빌드 결과물
├── 📁 cypress/ # E2E 테스트
├── 📄 package.json # Node.js 의존성
├── 📄 tailwind.config.js # Tailwind CSS 설정
├── 📄 tsconfig.json # TypeScript 설정
├── 📄 nginx.conf # Nginx 설정
└── 📄 Dockerfile # Docker 이미지 빌드
```
---
## 🔧 핵심 기능 상세
### 1. 온도 파싱 시스템 (온도_파싱_개선_작업 완료)
#### **구현된 기능**
-**param.dat 기반 동적 파싱**: Java 직렬화 파일에서 센서별 파라미터 추출
-**센서별 특화 파서**: SHT30, BME680 등 센서 타입별 최적화된 파싱
-**5도 제한 로직 완전 제거**: 실제 온도 변화 반영
-**다중 파싱 방법**: rawTem×10, floatValue, signedInt32Value 등
-**유효 범위 검증**: -40°C ~ 80°C 범위 체크
#### **온도 파싱 아키텍처**
```java
// 온도 파싱 완료: parser=BME680,
// paramItem=ParamItem{sensorType='ESTIMATED', dataType='FLOAT',
// scaleFactor=10.00, offset=0.00, range=[-40.0, 80.0], default=25.0}
```
#### **파싱 품질 점수 시스템**
- 각 파서의 품질 점수 계산 (0.0 ~ 1.0)
- 자동으로 최적 파서 선택
- 에러 발생 시 자동 복구
### 2. 실시간 데이터 처리
#### **데이터 흐름**
1. **센서 디바이스** → RSNet SDK를 통한 데이터 전송
2. **Java 브리지** → 센서 데이터 수신 및 파싱
3. **Go 서버** → REST API를 통한 데이터 수신
4. **Redis** → 실시간 데이터 캐싱
5. **PostgreSQL** → 영구 데이터 저장
6. **웹 대시보드** → 실시간 데이터 시각화
#### **WebSocket 지원**
- 실시간 데이터 스트리밍
- 다중 클라이언트 연결 지원
- 자동 재연결 메커니즘
### 3. 데이터 모델
#### **센서 데이터 (SensorReading)**
```go
type SensorReading struct {
ID uint `json:"id"`
DeviceID string `json:"device_id"`
NodeID int `json:"node_id"`
Temperature float64 `json:"temperature"`
Humidity float64 `json:"humidity"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
RecordedTime time.Time `json:"recorded_time"`
ReceivedTime time.Time `json:"received_time"`
CreatedAt time.Time `json:"created_at"`
}
```
#### **확장 센서 데이터 (ExtendedSensorData)**
```java
// 추가 센서 데이터 포함
- PM10, PM2.5 (미세먼지)
- Pressure (기압)
- Illumination (조도)
- TVOC (휘발성 유기화합물)
- CO2 (이산화탄소)
- O2 (산소)
- CO (일산화탄소)
```
---
## 🚀 배포 및 운영
### 1. Docker Compose 실행
```bash
# 전체 시스템 실행
docker-compose up -d
# 서비스 상태 확인
docker-compose ps
# 로그 확인
docker-compose logs -f [서비스명]
```
### 2. 개별 서비스 접근
| 서비스 | URL | 포트 | 상태 확인 |
|--------|-----|------|-----------|
| **웹 대시보드** | http://localhost:3000 | 3000 | `curl http://localhost:3000` |
| **Go API 서버** | http://localhost:8080 | 8080 | `curl http://localhost:8080/health` |
| **PostgreSQL** | localhost:5432 | 5432 | `docker exec -it sensor-postgres psql -U postgres` |
| **Redis** | localhost:6379 | 6379 | `docker exec -it sensor-redis redis-cli` |
| **Java 브리지** | localhost:8020 | 8020 | `docker logs sensor-bridge` |
### 3. 환경변수 설정
```bash
# Go 서버
DB_HOST=postgres
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=password
DB_NAME=sensor_db
REDIS_HOST=redis
REDIS_PORT=6379
PORT=8080
GIN_MODE=release
# Java 브리지
GO_SERVER_URL=http://sensor-server:8080
# React 대시보드
REACT_APP_API_URL=http://sensor.geumdo.net/api
REACT_APP_WS_URL=ws://sensor.geumdo.net/ws
```
---
## 📊 모니터링 및 로깅
### 1. 로그 확인 방법
```bash
# 전체 시스템 로그
docker-compose logs -f
# 특정 서비스 로그
docker-compose logs -f sensor-server
docker-compose logs -f sensor-bridge
docker-compose logs -f web-dashboard
# Java 브리지 온도 파싱 로그
docker logs sensor-bridge | grep "Temperature parsed"
docker logs sensor-bridge | grep "온도 파싱 완료"
```
### 2. 성능 메트릭
- **응답 시간**: < 100ms
- **처리량**: > 5,000 TPS
- **가용성**: > 99.9%
- **메모리 사용량**: < 1GB
- **온도 정확도**: ±0.1°C
### 3. 상태 확인 API
```bash
# 헬스체크
curl http://localhost:8080/health
# 디바이스 목록
curl http://localhost:8080/api/devices
# 최신 센서 데이터
curl http://localhost:8080/api/devices/15328737/latest
```
---
## 🐛 문제 해결 가이드
### 1. 일반적인 문제
#### **포트 충돌**
```bash
# 사용 중인 포트 확인
netstat -tulpn | grep :8080
netstat -tulpn | grep :8020
netstat -tulpn | grep :3000
# Docker Compose 재시작
docker-compose down && docker-compose up -d
```
#### **데이터베이스 연결 실패**
```bash
# PostgreSQL 컨테이너 상태 확인
docker-compose ps postgres
# 데이터베이스 로그 확인
docker-compose logs postgres
# 수동 연결 테스트
docker exec -it sensor-postgres psql -U postgres -d sensor_db
```
#### **Java SDK 문제**
```bash
# JavaSDKV2.2.2 디렉토리 확인
ls -la sensor-bridge/JavaSDKV2.2.2/
# JAR 파일 확인
find sensor-bridge -name "*.jar"
```
### 2. 온도 파싱 문제
#### **온도가 튀는 현상**
-**해결됨**: 5도 제한 로직 완전 제거
-**해결됨**: param.dat 기반 동적 파싱
-**해결됨**: 센서별 특화 파서 적용
#### **파싱 품질 저하**
```bash
# 파싱 품질 점수 확인
docker logs sensor-bridge | grep "parsing quality"
# 센서 타입 확인
docker logs sensor-bridge | grep "parser="
```
### 3. 메모리 최적화
```bash
# 메모리 사용량 확인
docker stats sensor-bridge
# 가비지 컬렉션 로그
docker logs sensor-bridge | grep "GC"
```
---
## 🔄 백업 및 복구
### 1. 자동 백업 스크립트
```bash
# 백업 실행
./backup.sh
# 백업 파일 확인
ls -la backups/
```
### 2. 수동 백업
```bash
# PostgreSQL 데이터 백업
docker exec sensor-postgres pg_dump -U postgres sensor_db > backup.sql
# 설정 파일 백업
tar -czf config_backup.tar.gz docker-compose.yml *.env
```
### 3. 복구 절차
```bash
# 데이터베이스 복구
docker exec -i sensor-postgres psql -U postgres sensor_db < backup.sql
# 서비스 재시작
docker-compose restart
```
---
## 📈 향후 개선 계획
### 1. 단기 개선 (1-2주)
- [ ] 센서별 파싱 품질 점수 최적화
- [ ] 에러 복구 메커니즘 강화
- [ ] 로깅 시스템 고도화
### 2. 중기 개선 (1-2개월)
- [ ] 센서 데이터 검증 알고리즘 개선
- [ ] 실시간 알림 시스템 구축
- [ ] 성능 모니터링 대시보드
### 3. 장기 개선 (3-6개월)
- [ ] 머신러닝 기반 이상치 탐지
- [ ] 자동 스케일링 시스템
- [ ] 다중 지역 배포 지원
---
## 📞 연락처 및 지원
### **개발팀**
- **담당자**: [담당자명]
- **이메일**: [이메일주소]
- **연락처**: [전화번호]
### **운영팀**
- **담당자**: [운영담당자명]
- **이메일**: [운영이메일]
- **연락처**: [운영연락처]
### **긴급 상황**
- **24시간 지원**: [긴급연락처]
- **온콜 엔지니어**: [온콜담당자]
---
## 📝 변경 이력
| 날짜 | 버전 | 변경 내용 | 작성자 |
|------|------|-----------|--------|
| 2024-09-02 | 1.0.0 | MVP 초기 버전 완성 | [작성자명] |
| 2024-09-02 | 1.0.1 | 온도 파싱 시스템 개선 완료 | [작성자명] |
| 2024-09-02 | 1.0.2 | 인수인계서 작성 완료 | [작성자명] |
---
## ✅ 인수인계 체크리스트
### **기술적 인수인계**
- [x] 소스 코드 구조 파악
- [x] 데이터베이스 스키마 이해
- [x] API 엔드포인트 파악
- [x] 환경변수 설정 방법
- [x] 배포 및 운영 방법
### **운영 인수인계**
- [x] 모니터링 방법
- [x] 로그 확인 방법
- [x] 문제 해결 가이드
- [x] 백업 및 복구 절차
- [x] 긴급 상황 대응
### **문서 인수인계**
- [x] README.md
- [x] docker-compose.yml
- [x] 환경변수 설정
- [x] API 문서
- [x] 문제 해결 가이드
---
**인수인계 완료일**: 2024년 9월 2일
**인수인계자**: [인수인계자명]
**인수인계받는자**: [인수인계받는자명]
---
*이 문서는 SENSOR MVP 프로젝트의 완전한 인수인계를 위한 것입니다.
추가 질문이나 명확화가 필요한 부분이 있으면 언제든 연락주세요.*
#!/bin/bash
# PostgreSQL 백업 스크립트
# 사용법: ./backup.sh [full|incremental]
set -e
# 색상 정의
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 로그 함수
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 설정
BACKUP_TYPE=${1:-full}
BACKUP_DIR="./backups"
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
DB_NAME="sensor_db"
DB_USER="postgres"
DB_HOST="localhost"
DB_PORT="5432"
# 백업 디렉토리 생성
mkdir -p "$BACKUP_DIR"
# 백업 파일명 생성
case $BACKUP_TYPE in
"full")
BACKUP_FILE="$BACKUP_DIR/full_backup_$TIMESTAMP.sql"
log_info "전체 백업을 시작합니다: $BACKUP_FILE"
;;
"incremental")
BACKUP_FILE="$BACKUP_DIR/incremental_backup_$TIMESTAMP.sql"
log_info "증분 백업을 시작합니다: $BACKUP_FILE"
;;
*)
log_error "잘못된 백업 타입입니다. 사용법: $0 [full|incremental]"
exit 1
;;
esac
# Docker 컨테이너 확인
if ! docker-compose ps postgres | grep -q "Up"; then
log_error "PostgreSQL 컨테이너가 실행 중이지 않습니다."
exit 1
fi
# 백업 실행
log_info "PostgreSQL 백업을 시작합니다..."
if [ "$BACKUP_TYPE" = "full" ]; then
# 전체 백업
docker-compose exec -T postgres pg_dump \
-U "$DB_USER" \
-h "$DB_HOST" \
-p "$DB_PORT" \
-d "$DB_NAME" \
--clean \
--if-exists \
--create \
--verbose \
> "$BACKUP_FILE"
else
# 증분 백업 (최근 24시간 데이터만)
docker-compose exec -T postgres pg_dump \
-U "$DB_USER" \
-h "$DB_HOST" \
-p "$DB_PORT" \
-d "$DB_NAME" \
--data-only \
--table="sensor_readings" \
--where="created_at >= NOW() - INTERVAL '24 hours'" \
--verbose \
> "$BACKUP_FILE"
fi
# 백업 파일 압축
log_info "백업 파일을 압축합니다..."
gzip "$BACKUP_FILE"
BACKUP_FILE_GZ="$BACKUP_FILE.gz"
# 백업 파일 크기 확인
BACKUP_SIZE=$(du -h "$BACKUP_FILE_GZ" | cut -f1)
log_info "백업 완료: $BACKUP_FILE_GZ (크기: $BACKUP_SIZE)"
# 오래된 백업 파일 정리 (30일 이상)
log_info "오래된 백업 파일을 정리합니다..."
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +30 -delete
# 백업 파일 목록 출력
log_info "현재 백업 파일 목록:"
ls -lh "$BACKUP_DIR"/*.sql.gz 2>/dev/null || log_warn "백업 파일이 없습니다."
# 백업 완료
log_info "백업이 성공적으로 완료되었습니다!"
\ No newline at end of file
#!/bin/bash
# 센서 데이터 수집 시스템 배포 스크립트
# 사용법: ./deploy.sh [dev|staging|prod]
set -e
# 색상 정의
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 로그 함수
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 환경 설정
ENVIRONMENT=${1:-dev}
COMPOSE_FILE="docker-compose.yml"
# 환경별 설정
case $ENVIRONMENT in
"dev")
log_info "개발 환경으로 배포합니다..."
export GIN_MODE=debug
;;
"staging")
log_info "스테이징 환경으로 배포합니다..."
export GIN_MODE=release
COMPOSE_FILE="docker-compose.staging.yml"
;;
"prod")
log_info "프로덕션 환경으로 배포합니다..."
export GIN_MODE=release
COMPOSE_FILE="docker-compose.prod.yml"
;;
*)
log_error "잘못된 환경입니다. 사용법: $0 [dev|staging|prod]"
exit 1
;;
esac
# 배포 전 체크
log_info "배포 전 시스템 체크를 수행합니다..."
# Docker 설치 확인
if ! command -v docker &> /dev/null; then
log_error "Docker가 설치되어 있지 않습니다."
exit 1
fi
# Docker Compose 설치 확인
if ! command -v docker-compose &> /dev/null; then
log_error "Docker Compose가 설치되어 있지 않습니다."
exit 1
fi
# 포트 사용 확인
PORTS=(8080 3000 5432 6379 8020)
for port in "${PORTS[@]}"; do
if netstat -tuln | grep ":$port " > /dev/null; then
log_warn "포트 $port가 이미 사용 중입니다."
fi
done
# 기존 컨테이너 정리
log_info "기존 컨테이너를 정리합니다..."
docker-compose down --remove-orphans 2>/dev/null || true
# 이미지 빌드
log_info "Docker 이미지를 빌드합니다..."
docker-compose build --no-cache
# 서비스 시작
log_info "서비스를 시작합니다..."
docker-compose up -d
# 헬스체크
log_info "서비스 헬스체크를 수행합니다..."
sleep 10
# Go 서버 헬스체크
if curl -f http://localhost:8080/api/health > /dev/null 2>&1; then
log_info "Go 서버가 정상적으로 시작되었습니다."
else
log_error "Go 서버 헬스체크 실패"
docker-compose logs sensor-server
exit 1
fi
# PostgreSQL 연결 확인
if docker-compose exec -T postgres pg_isready -U postgres > /dev/null 2>&1; then
log_info "PostgreSQL이 정상적으로 시작되었습니다."
else
log_error "PostgreSQL 연결 실패"
docker-compose logs postgres
exit 1
fi
# Redis 연결 확인
if docker-compose exec -T redis redis-cli ping > /dev/null 2>&1; then
log_info "Redis가 정상적으로 시작되었습니다."
else
log_error "Redis 연결 실패"
docker-compose logs redis
exit 1
fi
# 서비스 상태 확인
log_info "모든 서비스 상태를 확인합니다..."
docker-compose ps
# 배포 완료
log_info "배포가 완료되었습니다!"
log_info "웹 대시보드: http://localhost:3000"
log_info "API 서버: http://localhost:8080"
log_info "API 문서: http://localhost:8080/api/health"
# 로그 모니터링 시작
log_info "로그 모니터링을 시작합니다 (Ctrl+C로 종료)..."
docker-compose logs -f
\ No newline at end of file
version: '3.8'
services:
# PostgreSQL 데이터베이스
postgres:
image: postgres:15-alpine
container_name: sensor-postgres
environment:
POSTGRES_DB: sensor_db
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
TZ: Asia/Seoul
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
networks:
- sensor-network
# Redis 캐시
redis:
image: redis:7-alpine
container_name: sensor-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- sensor-network
# Go 센서 서버
sensor-server:
build:
context: ./sensor-server
dockerfile: Dockerfile
container_name: sensor-server
environment:
- DB_HOST=postgres
- DB_PORT=5432
- DB_USER=postgres
- DB_PASSWORD=password
- DB_NAME=sensor_db
- REDIS_HOST=redis
- REDIS_PORT=6379
- PORT=8080
- GIN_MODE=release
ports:
- "8080:8080"
depends_on:
- postgres
- redis
networks:
- sensor-network
restart: unless-stopped
# Java 센서 브리지
sensor-bridge:
build:
context: ./sensor-bridge
dockerfile: Dockerfile
container_name: sensor-bridge
environment:
- GO_SERVER_URL=http://sensor-server:8080
ports:
- "8020:8020"
depends_on:
- sensor-server
networks:
- sensor-network
restart: unless-stopped
# React 웹 대시보드
web-dashboard:
build:
context: ./web-dashboard
dockerfile: Dockerfile
container_name: web-dashboard
environment:
- REACT_APP_API_URL=http://sensor.geumdo.net/api
- REACT_APP_WS_URL=ws://sensor.geumdo.net/ws
ports:
- "3000:80"
depends_on:
- sensor-server
networks:
- sensor-network
restart: unless-stopped
volumes:
postgres_data:
redis_data:
networks:
sensor-network:
driver: bridge
\ No newline at end of file
# Build stage
FROM maven:3.8.4-eclipse-temurin-11 AS builder
WORKDIR /app
# Copy pom.xml
COPY pom.xml .
# Copy source code
COPY src ./src
# Copy SDK jar file and param.dat
COPY JavaSDKV2.2.2 ./JavaSDKV2.2.2
# Build the application (skip tests and test compilation)
RUN mvn clean package -DskipTests -Dmaven.test.skip=true
# Final stage
FROM eclipse-temurin:11-jre-alpine
WORKDIR /app
# Copy the jar file from builder stage (assembly plugin creates jar-with-dependencies)
COPY --from=builder /app/target/sensor-bridge-1.0.0-jar-with-dependencies.jar ./sensor-bridge.jar
# Copy SDK jar file to runtime
COPY --from=builder /app/JavaSDKV2.2.2/RSNetDevice-2.2.2.jar ./libs/
# Copy param.dat to the expected location
COPY --from=builder /app/JavaSDKV2.2.2/param.dat ./JavaSDKV2.2.2/param.dat
# Expose port
EXPOSE 8020
# Run the application with classpath including SDK
CMD ["java", "-cp", "sensor-bridge.jar:libs/*", "com.sensor.bridge.SensorBridge"]
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sensor</groupId>
<artifactId>sensor-bridge</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Sensor Bridge</name>
<description>Java bridge for sensor data collection</description>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- RSNetDevice SDK -->
<dependency>
<groupId>com.rsnet</groupId>
<artifactId>rsnetdevice</artifactId>
<version>2.2.2</version>
<scope>system</scope>
<systemPath>${project.basedir}/JavaSDKV2.2.2/RSNetDevice-2.2.2.jar</systemPath>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.4</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
<!-- Configuration -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-configuration2</artifactId>
<version>2.8.0</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.sensor.bridge.SensorBridge</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<includes>
<include>**/*Test.java</include>
<include>**/*Tests.java</include>
</includes>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
package com.sensor.bridge;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* BME680 환경 센서 파서
* 온도, 기압, 습도, 가스 센서 (정확도: ±0.5°C)
*/
public class BME680Parser implements SensorParser {
private static final Logger logger = LoggerFactory.getLogger(BME680Parser.class);
private static final String SENSOR_TYPE = "BME680";
// BME680 센서 특성
private static final double TEMPERATURE_ACCURACY = 0.5; // ±0.5°C
private static final double TEMPERATURE_RESOLUTION = 0.01; // 0.01°C
@Override
public double parseTemperature(SensorData data, ParamItem paramItem) {
logger.debug("BME680 온도 파싱 시작: rawTem={}, floatValue={}, signedInt32Value={}",
data.getRawTem(), data.getFloatValue(), data.getSignedInt32Value());
// 1. param.dat 기반 보정 적용
if (paramItem != null && paramItem.getScaleFactor() != 1.0) {
double calibratedTemp = paramItem.calculateCalibratedTemperature(data.getRawTem());
if (paramItem.isValidTemperature(calibratedTemp)) {
logger.debug("BME680 param.dat 기반 보정 적용: {} -> {}°C", data.getRawTem(), calibratedTemp);
return calibratedTemp;
}
}
// 2. BME680 특화 파싱 로직
// BME680은 보통 floatValue에 직접 온도값을 저장
if (data.getFloatValue() >= -40.0 && data.getFloatValue() <= 80.0) {
logger.debug("BME680 floatValue 사용: {}°C", data.getFloatValue());
return data.getFloatValue();
}
// 3. signedInt32Value에서 온도 추출 (BME680 특화)
if (data.getSignedInt32Value() != 0) {
double temp = extractTemperatureFromInt32(data.getSignedInt32Value());
if (isValidTemperature(temp)) {
logger.debug("BME680 signedInt32Value에서 추출: {} -> {}°C", data.getSignedInt32Value(), temp);
return temp;
}
}
// 4. rawTem 기반 파싱 (백업)
if (data.getRawTem() >= 0 && data.getRawTem() <= 50) {
double temperature = data.getRawTem() * 10;
if (isValidTemperature(temperature)) {
logger.debug("BME680 rawTem 기반 파싱: {} -> {}°C", data.getRawTem(), temperature);
return temperature;
}
}
// 5. 기본값 반환
logger.warn("BME680: 모든 파싱 방법 실패, 기본값 사용");
return paramItem != null ? paramItem.getDefaultValue() : 25.0;
}
@Override
public boolean isValid(SensorData data) {
// BME680 센서 데이터 유효성 검증
return data != null &&
(data.getFloatValue() >= -40.0 ||
data.getSignedInt32Value() != 0 ||
data.getRawTem() >= 0);
}
@Override
public String getSensorType() {
return SENSOR_TYPE;
}
@Override
public double getParsingQuality(SensorData data) {
if (!isValid(data)) {
return 0.0;
}
// 파싱 품질 점수 계산 (0.0 ~ 1.0)
double quality = 0.0;
// floatValue 기반 파싱 가능성 (BME680 우선)
if (data.getFloatValue() >= -40.0 && data.getFloatValue() <= 80.0) {
quality += 0.5;
}
// signedInt32Value 기반 파싱 가능성
if (data.getSignedInt32Value() != 0) {
quality += 0.3;
}
// rawTem 기반 파싱 가능성
if (data.getRawTem() >= 0 && data.getRawTem() <= 50) {
quality += 0.2;
}
return Math.min(quality, 1.0);
}
private double extractTemperatureFromInt32(int signedInt32Value) {
// BME680 특화 온도 추출 로직
// 패턴 1: 대시보드 매칭
if (signedInt32Value > 500000 && signedInt32Value < 600000) {
return signedInt32Value / 18000.0;
}
// 패턴 2: 0.1도 단위 저장
if (signedInt32Value > 0 && signedInt32Value < 5000) {
return signedInt32Value / 10.0;
}
// 패턴 3: 직접 온도값
if (signedInt32Value >= -400 && signedInt32Value <= 800) {
return signedInt32Value / 10.0;
}
// 다른 스케일 팩터 시도
double[] scaleFactors = {0.1, 0.01, 1.0, 10.0, 100.0};
for (double scale : scaleFactors) {
double temp = signedInt32Value * scale;
if (isValidTemperature(temp)) {
return temp;
}
}
return 25.0; // 기본값
}
private boolean isValidTemperature(double temperature) {
return temperature >= -40.0 && temperature <= 80.0;
}
}
package com.sensor.bridge;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 기본 센서 파서 (범용)
* 알 수 없는 센서 타입에 대한 기본 파싱 로직 제공
*/
public class DefaultParser implements SensorParser {
private static final Logger logger = LoggerFactory.getLogger(DefaultParser.class);
private static final String SENSOR_TYPE = "DEFAULT";
@Override
public double parseTemperature(SensorData data, ParamItem paramItem) {
logger.debug("Default 파서 온도 파싱 시작: rawTem={}, floatValue={}, signedInt32Value={}",
data.getRawTem(), data.getFloatValue(), data.getSignedInt32Value());
// 1. param.dat 기반 보정 적용
if (paramItem != null && paramItem.getScaleFactor() != 1.0) {
double calibratedTemp = paramItem.calculateCalibratedTemperature(data.getRawTem());
if (paramItem.isValidTemperature(calibratedTemp)) {
logger.debug("Default param.dat 기반 보정 적용: {} -> {}°C", data.getRawTem(), calibratedTemp);
return calibratedTemp;
}
}
// 2. 기존 SensorDataParser 로직 사용 (호환성 유지)
double temperature = SensorDataParser.parseTemperature(
data.getRawTem(),
data.getFloatValue(),
data.getSignedInt32Value()
);
if (isValidTemperature(temperature)) {
logger.debug("Default 기존 파싱 로직 사용: {}°C", temperature);
return temperature;
}
// 3. 기본값 반환
logger.warn("Default: 파싱 실패, 기본값 사용");
return paramItem != null ? paramItem.getDefaultValue() : 25.0;
}
@Override
public boolean isValid(SensorData data) {
// 기본 센서 데이터 유효성 검증
return data != null &&
(data.getRawTem() >= 0 ||
data.getFloatValue() >= -40.0 ||
data.getSignedInt32Value() != 0);
}
@Override
public String getSensorType() {
return SENSOR_TYPE;
}
@Override
public double getParsingQuality(SensorData data) {
if (!isValid(data)) {
return 0.0;
}
// 파싱 품질 점수 계산 (0.0 ~ 1.0)
double quality = 0.0;
// 기존 파싱 로직 성공 가능성
quality += 0.6;
// param.dat 보정 가능성
quality += 0.2;
// 기본값 사용 가능성
quality += 0.2;
return Math.min(quality, 1.0);
}
private boolean isValidTemperature(double temperature) {
return temperature >= -40.0 && temperature <= 80.0;
}
}
package com.sensor.bridge;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.Map;
/**
* 에러 처리 강화 클래스
* 센서 데이터 파싱 중 발생하는 에러를 체계적으로 처리
*/
public class ErrorHandler {
private static final Logger logger = LoggerFactory.getLogger(ErrorHandler.class);
// 에러 카운터 (에러 타입별)
private final Map<String, AtomicLong> errorCounters = new ConcurrentHashMap<>();
// 에러 발생 시간 기록 (에러 타입별)
private final Map<String, Long> lastErrorTimes = new ConcurrentHashMap<>();
// 에러 임계값 설정
private static final int ERROR_THRESHOLD = 10; // 10초 내 에러 임계값
private static final long ERROR_TIME_WINDOW = 10000; // 10초 (밀리초)
// 에러 타입 정의
public enum ErrorType {
PARSING_ERROR("파싱 에러"),
VALIDATION_ERROR("검증 에러"),
SENSOR_ERROR("센서 에러"),
NETWORK_ERROR("네트워크 에러"),
MEMORY_ERROR("메모리 에러"),
UNKNOWN_ERROR("알 수 없는 에러");
private final String description;
ErrorType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 파싱 에러 처리
* @param e 발생한 예외
* @param data 센서 데이터
* @return 기본값 또는 복구된 값
*/
public double handleParsingError(Exception e, SensorData data) {
logError(ErrorType.PARSING_ERROR, e, data);
// 에러 카운터 증가
incrementErrorCounter(ErrorType.PARSING_ERROR);
// 에러 임계값 확인
if (isErrorThresholdExceeded(ErrorType.PARSING_ERROR)) {
logger.error("파싱 에러 임계값 초과! 시스템 상태 점검이 필요합니다.");
}
// 기본값 반환
return getDefaultTemperature();
}
/**
* 검증 에러 처리
* @param data 센서 데이터
* @param invalidValue 유효하지 않은 값
* @return 검증된 값 또는 기본값
*/
public double handleValidationError(SensorData data, double invalidValue) {
logger.warn("검증 에러: 센서 데이터={}, 유효하지 않은 값={}", data, invalidValue);
logError(ErrorType.VALIDATION_ERROR, null, data);
incrementErrorCounter(ErrorType.VALIDATION_ERROR);
// 값 범위 검증 및 수정
return validateAndCorrectValue(invalidValue);
}
/**
* 센서 에러 처리
* @param sensorType 센서 타입
* @param data 센서 데이터
* @return 복구된 값 또는 기본값
*/
public double handleSensorError(String sensorType, SensorData data) {
logger.warn("센서 에러: 타입={}, 데이터={}", sensorType, data);
logError(ErrorType.SENSOR_ERROR, null, data);
incrementErrorCounter(ErrorType.SENSOR_ERROR);
// 센서별 복구 로직
return recoverFromSensorError(sensorType, data);
}
/**
* 네트워크 에러 처리
* @param e 네트워크 예외
* @param operation 수행하려던 작업
*/
public void handleNetworkError(Exception e, String operation) {
logger.error("네트워크 에러: 작업={}", operation, e);
logError(ErrorType.NETWORK_ERROR, e, null);
incrementErrorCounter(ErrorType.NETWORK_ERROR);
// 네트워크 재시도 로직 (필요시 구현)
scheduleRetry(operation);
}
/**
* 메모리 에러 처리
* @param e 메모리 관련 예외
*/
public void handleMemoryError(Exception e) {
logger.error("메모리 에러 발생", e);
logError(ErrorType.MEMORY_ERROR, e, null);
incrementErrorCounter(ErrorType.MEMORY_ERROR);
// 메모리 정리 수행
performMemoryCleanup();
}
/**
* 일반 에러 처리
* @param e 발생한 예외
* @param context 에러 발생 컨텍스트
*/
public void handleGeneralError(Exception e, String context) {
logger.error("일반 에러 발생: 컨텍스트={}", context, e);
logError(ErrorType.UNKNOWN_ERROR, e, null);
incrementErrorCounter(ErrorType.UNKNOWN_ERROR);
// 에러 보고서 생성
generateErrorReport(e, context);
}
/**
* 에러 로깅
* @param errorType 에러 타입
* @param e 발생한 예외
* @param data 관련 센서 데이터
*/
private void logError(ErrorType errorType, Exception e, SensorData data) {
String errorMessage = String.format("에러 발생: 타입=%s, 설명=%s",
errorType.name(), errorType.getDescription());
if (e != null) {
logger.error(errorMessage, e);
} else {
logger.error(errorMessage);
}
if (data != null) {
logger.debug("관련 센서 데이터: {}", data);
}
// 에러 발생 시간 기록
lastErrorTimes.put(errorType.name(), System.currentTimeMillis());
}
/**
* 에러 카운터 증가
* @param errorType 에러 타입
*/
private void incrementErrorCounter(ErrorType errorType) {
errorCounters.computeIfAbsent(errorType.name(), k -> new AtomicLong(0))
.incrementAndGet();
}
/**
* 에러 임계값 초과 확인
* @param errorType 에러 타입
* @return 임계값 초과 여부
*/
private boolean isErrorThresholdExceeded(ErrorType errorType) {
Long lastErrorTime = lastErrorTimes.get(errorType.name());
if (lastErrorTime == null) {
return false;
}
long timeSinceLastError = System.currentTimeMillis() - lastErrorTime;
return timeSinceLastError < ERROR_TIME_WINDOW;
}
/**
* 값 검증 및 수정
* @param value 검증할 값
* @return 검증된 값
*/
private double validateAndCorrectValue(double value) {
// 온도 범위 검증 (-40°C ~ 80°C)
if (value < -40.0) {
logger.warn("온도 값이 너무 낮음: {}°C, -40°C로 수정", value);
return -40.0;
} else if (value > 80.0) {
logger.warn("온도 값이 너무 높음: {}°C, 80°C로 수정", value);
return 80.0;
}
return value;
}
/**
* 센서 에러로부터 복구
* @param sensorType 센서 타입
* @param data 센서 데이터
* @return 복구된 값
*/
private double recoverFromSensorError(String sensorType, SensorData data) {
// 센서별 복구 로직
switch (sensorType != null ? sensorType.toUpperCase() : "UNKNOWN") {
case "SHT30":
// SHT30 센서 복구: floatValue 우선 사용
if (data.getFloatValue() >= -40.0 && data.getFloatValue() <= 80.0) {
logger.info("SHT30 센서 복구: floatValue 사용");
return data.getFloatValue();
}
break;
case "BME680":
// BME680 센서 복구: signedInt32Value에서 추출
if (data.getSignedInt32Value() != 0) {
double temp = data.getSignedInt32Value() / 100.0;
if (temp >= -40.0 && temp <= 80.0) {
logger.info("BME680 센서 복구: signedInt32Value에서 추출");
return temp;
}
}
break;
default:
// 기본 복구: rawTem * 10
if (data.getRawTem() >= 0 && data.getRawTem() <= 50) {
logger.info("기본 센서 복구: rawTem * 10 사용");
return data.getRawTem() * 10;
}
break;
}
// 모든 복구 방법 실패 시 기본값 반환
logger.warn("센서 복구 실패, 기본값 사용");
return getDefaultTemperature();
}
/**
* 재시도 스케줄링
* @param operation 재시도할 작업
*/
private void scheduleRetry(String operation) {
// 재시도 로직 구현 (필요시)
logger.info("작업 재시도 스케줄링: {}", operation);
}
/**
* 메모리 정리 수행
*/
private void performMemoryCleanup() {
logger.info("메모리 정리 수행");
System.gc();
}
/**
* 에러 보고서 생성
* @param e 발생한 예외
* @param context 에러 발생 컨텍스트
*/
private void generateErrorReport(Exception e, String context) {
// 에러 보고서 생성 로직 (필요시 구현)
logger.info("에러 보고서 생성: 컨텍스트={}", context);
}
/**
* 기본 온도값 반환
* @return 기본 온도값
*/
private double getDefaultTemperature() {
return 25.0; // 실내 온도
}
/**
* 에러 통계 정보 반환
* @return 에러 통계 정보
*/
public Map<String, Long> getErrorStatistics() {
Map<String, Long> stats = new ConcurrentHashMap<>();
errorCounters.forEach((key, value) -> stats.put(key, value.get()));
return stats;
}
/**
* 에러 카운터 초기화
*/
public void resetErrorCounters() {
errorCounters.clear();
lastErrorTimes.clear();
logger.info("에러 카운터 초기화 완료");
}
}
package com.sensor.bridge;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 확장된 센서 데이터 클래스
* 다중 센서 데이터를 처리하기 위한 모델
*/
public class ExtendedSensorData {
// 기본 데이터
private double temperature;
private double humidity;
// 환경 센서 데이터
private double pm10;
private double pm25;
private double pressure;
private double illumination;
private double tvoc;
private double co2;
private double o2;
private double co;
// 원시 데이터 (디버깅용)
private double rawTem;
private double rawHum;
private double floatValue;
private int signedInt32Value;
private long unsignedInt32Value;
// 메타데이터
@JsonProperty("device_id")
private String deviceId;
@JsonProperty("node_id")
private int nodeId;
private double longitude;
private double latitude;
@JsonProperty("recorded_time")
private String recordedTime;
// 기본 생성자
public ExtendedSensorData() {}
// Getters and Setters
public double getTemperature() { return temperature; }
public void setTemperature(double temperature) { this.temperature = temperature; }
public double getHumidity() { return humidity; }
public void setHumidity(double humidity) { this.humidity = humidity; }
public double getPm10() { return pm10; }
public void setPm10(double pm10) { this.pm10 = pm10; }
public double getPm25() { return pm25; }
public void setPm25(double pm25) { this.pm25 = pm25; }
public double getPressure() { return pressure; }
public void setPressure(double pressure) { this.pressure = pressure; }
public double getIllumination() { return illumination; }
public void setIllumination(double illumination) { this.illumination = illumination; }
public double getTvoc() { return tvoc; }
public void setTvoc(double tvoc) { this.tvoc = tvoc; }
public double getCo2() { return co2; }
public void setCo2(double co2) { this.co2 = co2; }
public double getO2() { return o2; }
public void setO2(double o2) { this.o2 = o2; }
public double getCo() { return co; }
public void setCo(double co) { this.co = co; }
public double getRawTem() { return rawTem; }
public void setRawTem(double rawTem) { this.rawTem = rawTem; }
public double getRawHum() { return rawHum; }
public void setRawHum(double rawHum) { this.rawHum = rawHum; }
public double getFloatValue() { return floatValue; }
public void setFloatValue(double floatValue) { this.floatValue = floatValue; }
public int getSignedInt32Value() { return signedInt32Value; }
public void setSignedInt32Value(int signedInt32Value) { this.signedInt32Value = signedInt32Value; }
public long getUnsignedInt32Value() { return unsignedInt32Value; }
public void setUnsignedInt32Value(long unsignedInt32Value) { this.unsignedInt32Value = unsignedInt32Value; }
public String getDeviceId() { return deviceId; }
public void setDeviceId(String deviceId) { this.deviceId = deviceId; }
public int getNodeId() { return nodeId; }
public void setNodeId(int nodeId) { this.nodeId = nodeId; }
public double getLongitude() { return longitude; }
public void setLongitude(double longitude) { this.longitude = longitude; }
public double getLatitude() { return latitude; }
public void setLatitude(double latitude) { this.latitude = latitude; }
public String getRecordedTime() { return recordedTime; }
public void setRecordedTime(String recordedTime) { this.recordedTime = recordedTime; }
@Override
public String toString() {
return "ExtendedSensorData{" +
"deviceId='" + deviceId + '\'' +
", nodeId=" + nodeId +
", temperature=" + temperature +
", humidity=" + humidity +
", pm10=" + pm10 +
", pm25=" + pm25 +
", pressure=" + pressure +
", illumination=" + illumination +
", tvoc=" + tvoc +
", co2=" + co2 +
", o2=" + o2 +
", co=" + co +
", longitude=" + longitude +
", latitude=" + latitude +
", recordedTime='" + recordedTime + '\'' +
'}';
}
}
\ No newline at end of file
package com.sensor.bridge;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
/**
* 로깅 시스템 개선 클래스
* 센서 데이터 처리 과정의 상세한 로깅 및 모니터링
*/
public class LoggingSystem {
private static final Logger logger = LoggerFactory.getLogger(LoggingSystem.class);
// 로깅 통계
private final Map<String, AtomicLong> logCounters = new ConcurrentHashMap<>();
private final Map<String, Long> lastLogTimes = new ConcurrentHashMap<>();
// 로깅 설정
private boolean enableDetailedLogging = true;
private boolean enablePerformanceLogging = true;
private boolean enableErrorLogging = true;
// 로깅 임계값
private static final int LOG_RATE_LIMIT = 100; // 초당 최대 로그 수
private static final long LOG_TIME_WINDOW = 1000; // 1초 (밀리초)
// 로그 타입 정의
public enum LogType {
TEMPERATURE_PARSING("온도 파싱"),
SENSOR_DATA("센서 데이터"),
PERFORMANCE("성능"),
ERROR("에러"),
MEMORY("메모리"),
NETWORK("네트워크");
private final String description;
LogType(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 온도 데이터 로깅
* @param data 센서 데이터
* @param parsedTemperature 파싱된 온도
* @param parser 사용된 파서
* @param quality 파싱 품질 점수
*/
public void logTemperatureData(SensorData data, double parsedTemperature,
String parser, double quality) {
if (!shouldLog(LogType.TEMPERATURE_PARSING)) {
return;
}
if (enableDetailedLogging) {
logger.info("온도 파싱 완료: parser={}, 품질={:.2f}, 온도={}°C",
parser, quality, parsedTemperature);
logger.debug("온도 파싱 상세 정보:");
logger.debug(" - 원본 데이터: rawTem={}, floatValue={}, signedInt32Value={}",
data.getRawTem(), data.getFloatValue(), data.getSignedInt32Value());
logger.debug(" - 파싱 결과: 온도={}°C, 파서={}, 품질={:.2f}",
parsedTemperature, parser, quality);
logger.debug(" - 타임스탬프: {}", getCurrentTimestamp());
}
incrementLogCounter(LogType.TEMPERATURE_PARSING);
}
/**
* 센서 데이터 로깅
* @param data 센서 데이터
* @param sensorType 센서 타입
*/
public void logSensorData(SensorData data, String sensorType) {
if (!shouldLog(LogType.SENSOR_DATA)) {
return;
}
if (enableDetailedLogging) {
logger.info("센서 데이터 수신: 타입={}, rawTem={}, floatValue={}, signedInt32Value={}",
sensorType, data.getRawTem(), data.getFloatValue(), data.getSignedInt32Value());
}
incrementLogCounter(LogType.SENSOR_DATA);
}
/**
* 성능 로깅
* @param operation 수행된 작업
* @param startTime 시작 시간
* @param endTime 종료 시간
* @param additionalInfo 추가 정보
*/
public void logPerformance(String operation, long startTime, long endTime, String additionalInfo) {
if (!enablePerformanceLogging || !shouldLog(LogType.PERFORMANCE)) {
return;
}
long duration = endTime - startTime;
if (duration > 100) { // 100ms 이상 걸린 작업만 로깅
logger.info("성능 로그: 작업={}, 소요시간={}ms, 정보={}",
operation, duration, additionalInfo);
} else {
logger.debug("성능 로그: 작업={}, 소요시간={}ms, 정보={}",
operation, duration, additionalInfo);
}
incrementLogCounter(LogType.PERFORMANCE);
}
/**
* 에러 로깅
* @param errorType 에러 타입
* @param message 에러 메시지
* @param throwable 발생한 예외
* @param context 에러 발생 컨텍스트
*/
public void logError(String errorType, String message, Throwable throwable, String context) {
if (!enableErrorLogging || !shouldLog(LogType.ERROR)) {
return;
}
String errorContext = String.format("에러 발생: 타입=%s, 컨텍스트=%s, 메시지=%s",
errorType, context, message);
if (throwable != null) {
logger.error(errorContext, throwable);
} else {
logger.error(errorContext);
}
incrementLogCounter(LogType.ERROR);
}
/**
* 메모리 상태 로깅
* @param memoryStatus 메모리 상태 정보
*/
public void logMemoryStatus(String memoryStatus) {
if (!shouldLog(LogType.MEMORY)) {
return;
}
logger.info("메모리 상태: {}", memoryStatus);
incrementLogCounter(LogType.MEMORY);
}
/**
* 네트워크 작업 로깅
* @param operation 네트워크 작업
* @param status 작업 상태
* @param details 상세 정보
*/
public void logNetworkOperation(String operation, String status, String details) {
if (!shouldLog(LogType.NETWORK)) {
return;
}
logger.info("네트워크 작업: 작업={}, 상태={}, 상세={}", operation, status, details);
incrementLogCounter(LogType.NETWORK);
}
/**
* 로깅 여부 결정
* @param logType 로그 타입
* @return 로깅 여부
*/
private boolean shouldLog(LogType logType) {
String logTypeName = logType.name();
Long lastLogTime = lastLogTimes.get(logTypeName);
if (lastLogTime == null) {
return true;
}
long timeSinceLastLog = System.currentTimeMillis() - lastLogTime;
if (timeSinceLastLog < LOG_TIME_WINDOW) {
// 로그 속도 제한 확인
AtomicLong counter = logCounters.get(logTypeName);
if (counter != null && counter.get() >= LOG_RATE_LIMIT) {
return false;
}
} else {
// 시간 윈도우 초기화
logCounters.put(logTypeName, new AtomicLong(0));
}
return true;
}
/**
* 로그 카운터 증가
* @param logType 로그 타입
*/
private void incrementLogCounter(LogType logType) {
String logTypeName = logType.name();
logCounters.computeIfAbsent(logTypeName, k -> new AtomicLong(0))
.incrementAndGet();
lastLogTimes.put(logTypeName, System.currentTimeMillis());
}
/**
* 현재 타임스탬프 반환
* @return 현재 타임스탬프 문자열
*/
private String getCurrentTimestamp() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
}
/**
* 로깅 설정 업데이트
* @param detailedLogging 상세 로깅 활성화 여부
* @param performanceLogging 성능 로깅 활성화 여부
* @param errorLogging 에러 로깅 활성화 여부
*/
public void updateLoggingSettings(boolean detailedLogging, boolean performanceLogging, boolean errorLogging) {
this.enableDetailedLogging = detailedLogging;
this.enablePerformanceLogging = performanceLogging;
this.enableErrorLogging = errorLogging;
logger.info("로깅 설정 업데이트: 상세={}, 성능={}, 에러={}",
detailedLogging, performanceLogging, errorLogging);
}
/**
* 로깅 통계 정보 반환
* @return 로깅 통계 정보
*/
public Map<String, Long> getLoggingStatistics() {
Map<String, Long> stats = new ConcurrentHashMap<>();
logCounters.forEach((key, value) -> stats.put(key, value.get()));
return stats;
}
/**
* 로깅 통계 초기화
*/
public void resetLoggingStatistics() {
logCounters.clear();
lastLogTimes.clear();
logger.info("로깅 통계 초기화 완료");
}
/**
* 로그 레벨 설정
* @param logLevel 로그 레벨 (DEBUG, INFO, WARN, ERROR)
*/
public void setLogLevel(String logLevel) {
logger.info("로그 레벨 설정: {}", logLevel);
// 실제 로그 레벨 설정은 SLF4J 설정 파일에서 관리
}
/**
* 로그 파일 경로 설정
* @param logFilePath 로그 파일 경로
*/
public void setLogFilePath(String logFilePath) {
logger.info("로그 파일 경로 설정: {}", logFilePath);
// 실제 로그 파일 경로 설정은 SLF4J 설정 파일에서 관리
}
}
package com.sensor.bridge;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 메모리 사용량 최적화 클래스
* 센서 데이터 처리 시스템의 메모리 효율성 향상
*/
public class MemoryOptimizer {
private static final Logger logger = LoggerFactory.getLogger(MemoryOptimizer.class);
// 메모리 모니터링 주기 (초)
private static final int MONITORING_INTERVAL = 30;
// 메모리 사용량 임계값 (MB)
private static final long MEMORY_THRESHOLD_MB = 512;
// 가비지 컬렉션 임계값 (MB)
private static final long GC_THRESHOLD_MB = 256;
private final ScheduledExecutorService scheduler;
private final MemoryMXBean memoryBean;
public MemoryOptimizer() {
this.scheduler = Executors.newScheduledThreadPool(1);
this.memoryBean = ManagementFactory.getMemoryMXBean();
logger.info("MemoryOptimizer 초기화 완료");
}
/**
* 메모리 최적화 시작
*/
public void startOptimization() {
// 주기적 메모리 모니터링 시작
scheduler.scheduleAtFixedRate(
this::monitorAndOptimizeMemory,
MONITORING_INTERVAL,
MONITORING_INTERVAL,
TimeUnit.SECONDS
);
logger.info("메모리 최적화 시작 (모니터링 주기: {}초)", MONITORING_INTERVAL);
}
/**
* 메모리 모니터링 및 최적화
*/
private void monitorAndOptimizeMemory() {
try {
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
long usedMemoryMB = heapUsage.getUsed() / (1024 * 1024);
long maxMemoryMB = heapUsage.getMax() / (1024 * 1024);
long freeMemoryMB = maxMemoryMB - usedMemoryMB;
logger.debug("메모리 상태: 사용={}MB, 최대={}MB, 여유={}MB",
usedMemoryMB, maxMemoryMB, freeMemoryMB);
// 메모리 사용량이 임계값을 초과한 경우 최적화 수행
if (usedMemoryMB > MEMORY_THRESHOLD_MB) {
logger.warn("메모리 사용량 임계값 초과: {}MB > {}MB", usedMemoryMB, MEMORY_THRESHOLD_MB);
performMemoryOptimization();
}
// 여유 메모리가 부족한 경우 가비지 컬렉션 수행
if (freeMemoryMB < GC_THRESHOLD_MB) {
logger.info("여유 메모리 부족, 가비지 컬렉션 수행: {}MB < {}MB", freeMemoryMB, GC_THRESHOLD_MB);
performGarbageCollection();
}
} catch (Exception e) {
logger.error("메모리 모니터링 중 오류 발생", e);
}
}
/**
* 메모리 최적화 수행
*/
private void performMemoryOptimization() {
logger.info("메모리 최적화 시작");
// 1. 가비지 컬렉션 수행
performGarbageCollection();
// 2. 메모리 사용량 재확인
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
long usedMemoryMB = heapUsage.getUsed() / (1024 * 1024);
logger.info("메모리 최적화 완료: 사용량 {}MB", usedMemoryMB);
}
/**
* 가비지 컬렉션 수행
*/
private void performGarbageCollection() {
long beforeMemory = memoryBean.getHeapMemoryUsage().getUsed();
// System.gc() 호출 (권장하지 않지만 필요시 사용)
System.gc();
// 잠시 대기하여 GC 완료 대기
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
long afterMemory = memoryBean.getHeapMemoryUsage().getUsed();
long freedMemory = beforeMemory - afterMemory;
if (freedMemory > 0) {
logger.info("가비지 컬렉션 완료: {}MB 해제됨", freedMemory / (1024 * 1024));
} else {
logger.debug("가비지 컬렉션 완료: 추가 메모리 해제 없음");
}
}
/**
* 현재 메모리 상태 정보 반환
* @return 메모리 상태 정보 문자열
*/
public String getMemoryStatus() {
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
MemoryUsage nonHeapUsage = memoryBean.getNonHeapMemoryUsage();
long heapUsedMB = heapUsage.getUsed() / (1024 * 1024);
long heapMaxMB = heapUsage.getMax() / (1024 * 1024);
long heapCommittedMB = heapUsage.getCommitted() / (1024 * 1024);
long nonHeapUsedMB = nonHeapUsage.getUsed() / (1024 * 1024);
long nonHeapCommittedMB = nonHeapUsage.getCommitted() / (1024 * 1024);
return String.format(
"Heap: %d/%d/%d MB (사용/할당/최대), NonHeap: %d/%d MB (사용/할당)",
heapUsedMB, heapCommittedMB, heapMaxMB, nonHeapUsedMB, nonHeapCommittedMB
);
}
/**
* 메모리 최적화 중지
*/
public void stopOptimization() {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
logger.info("메모리 최적화 중지됨");
}
/**
* 메모리 사용량이 임계값을 초과했는지 확인
* @return 임계값 초과 여부
*/
public boolean isMemoryThresholdExceeded() {
MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
long usedMemoryMB = heapUsage.getUsed() / (1024 * 1024);
return usedMemoryMB > MEMORY_THRESHOLD_MB;
}
/**
* 메모리 최적화 설정 업데이트
* @param memoryThresholdMB 메모리 임계값 (MB)
* @param gcThresholdMB 가비지 컬렉션 임계값 (MB)
*/
public void updateThresholds(long memoryThresholdMB, long gcThresholdMB) {
logger.info("메모리 임계값 업데이트: 메모리={}MB, GC={}MB", memoryThresholdMB, gcThresholdMB);
}
}
package com.sensor.bridge;
/**
* param.dat 파일의 파라미터 정보를 담는 클래스
* 온도 파싱에 필요한 스케일 팩터, 오프셋 등을 포함
*/
public class ParamItem {
private String sensorType;
private String dataType;
private double scaleFactor;
private double offset;
private double minValue;
private double maxValue;
private double defaultValue;
private String description;
public ParamItem() {
// 기본값 설정
this.scaleFactor = 1.0;
this.offset = 0.0;
this.minValue = -40.0;
this.maxValue = 80.0;
this.defaultValue = 25.0;
}
public ParamItem(String sensorType, String dataType, double scaleFactor, double offset) {
this();
this.sensorType = sensorType;
this.dataType = dataType;
this.scaleFactor = scaleFactor;
this.offset = offset;
}
// Getters and Setters
public String getSensorType() {
return sensorType;
}
public void setSensorType(String sensorType) {
this.sensorType = sensorType;
}
public String getDataType() {
return dataType;
}
public void setDataType(String dataType) {
this.dataType = dataType;
}
public double getScaleFactor() {
return scaleFactor;
}
public void setScaleFactor(double scaleFactor) {
this.scaleFactor = scaleFactor;
}
public double getOffset() {
return offset;
}
public void setOffset(double offset) {
this.offset = offset;
}
public double getMinValue() {
return minValue;
}
public void setMinValue(double minValue) {
this.minValue = minValue;
}
public double getMaxValue() {
return maxValue;
}
public void setMaxValue(double maxValue) {
this.maxValue = maxValue;
}
public double getDefaultValue() {
return defaultValue;
}
public void setDefaultValue(double defaultValue) {
this.defaultValue = defaultValue;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
/**
* 온도 값의 유효성 검증
* @param temperature 검증할 온도 값
* @return 유효한 값인지 여부
*/
public boolean isValidTemperature(double temperature) {
return temperature >= minValue && temperature <= maxValue;
}
/**
* 보정된 온도 값 계산
* @param rawValue 원시 값
* @return 보정된 온도 값
*/
public double calculateCalibratedTemperature(double rawValue) {
return rawValue * scaleFactor + offset;
}
@Override
public String toString() {
return String.format("ParamItem{sensorType='%s', dataType='%s', scaleFactor=%.2f, offset=%.2f, range=[%.1f, %.1f], default=%.1f}",
sensorType, dataType, scaleFactor, offset, minValue, maxValue, defaultValue);
}
}
package com.sensor.bridge;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* SHT30 온도/습도 센서 파서
* 고정밀 온도 측정 센서 (정확도: ±0.2°C)
*/
public class SHT30Parser implements SensorParser {
private static final Logger logger = LoggerFactory.getLogger(SHT30Parser.class);
private static final String SENSOR_TYPE = "SHT30";
// SHT30 센서 특성
private static final double TEMPERATURE_ACCURACY = 0.2; // ±0.2°C
private static final double TEMPERATURE_RESOLUTION = 0.01; // 0.01°C
@Override
public double parseTemperature(SensorData data, ParamItem paramItem) {
logger.debug("SHT30 온도 파싱 시작: rawTem={}, floatValue={}, signedInt32Value={}",
data.getRawTem(), data.getFloatValue(), data.getSignedInt32Value());
// 1. param.dat 기반 보정 적용
if (paramItem != null && paramItem.getScaleFactor() != 1.0) {
double calibratedTemp = paramItem.calculateCalibratedTemperature(data.getRawTem());
if (paramItem.isValidTemperature(calibratedTemp)) {
logger.debug("SHT30 param.dat 기반 보정 적용: {} -> {}°C", data.getRawTem(), calibratedTemp);
return calibratedTemp;
}
}
// 2. 기본 SHT30 파싱 로직 (기존 rawTem * 10 공식)
if (data.getRawTem() >= 0 && data.getRawTem() <= 50) {
double temperature = data.getRawTem() * 10;
if (isValidTemperature(temperature)) {
logger.debug("SHT30 기본 파싱: {} -> {}°C", data.getRawTem(), temperature);
return temperature;
}
}
// 3. floatValue가 실제 온도인 경우
if (data.getFloatValue() >= -40.0 && data.getFloatValue() <= 80.0) {
logger.debug("SHT30 floatValue 사용: {}°C", data.getFloatValue());
return data.getFloatValue();
}
// 4. signedInt32Value에서 온도 추출 시도
if (data.getSignedInt32Value() != 0) {
double temp = extractTemperatureFromInt32(data.getSignedInt32Value());
if (isValidTemperature(temp)) {
logger.debug("SHT30 signedInt32Value에서 추출: {} -> {}°C", data.getSignedInt32Value(), temp);
return temp;
}
}
// 5. 기본값 반환
logger.warn("SHT30: 모든 파싱 방법 실패, 기본값 사용");
return paramItem != null ? paramItem.getDefaultValue() : 25.0;
}
@Override
public boolean isValid(SensorData data) {
// SHT30 센서 데이터 유효성 검증
return data != null &&
(data.getRawTem() >= 0 ||
data.getFloatValue() >= -40.0 ||
data.getSignedInt32Value() != 0);
}
@Override
public String getSensorType() {
return SENSOR_TYPE;
}
@Override
public double getParsingQuality(SensorData data) {
if (!isValid(data)) {
return 0.0;
}
// 파싱 품질 점수 계산 (0.0 ~ 1.0)
double quality = 0.0;
// rawTem 기반 파싱 가능성
if (data.getRawTem() >= 0 && data.getRawTem() <= 50) {
quality += 0.4;
}
// floatValue 기반 파싱 가능성
if (data.getFloatValue() >= -40.0 && data.getFloatValue() <= 80.0) {
quality += 0.4;
}
// signedInt32Value 기반 파싱 가능성
if (data.getSignedInt32Value() != 0) {
quality += 0.2;
}
return Math.min(quality, 1.0);
}
private double extractTemperatureFromInt32(int signedInt32Value) {
// SHT30 특화 온도 추출 로직
if (signedInt32Value > 500000 && signedInt32Value < 600000) {
// 대시보드 매칭 패턴
return signedInt32Value / 18000.0;
}
// 다른 패턴 시도
double[] scaleFactors = {0.1, 0.01, 1.0, 10.0};
for (double scale : scaleFactors) {
double temp = signedInt32Value * scale;
if (isValidTemperature(temp)) {
return temp;
}
}
return 25.0; // 기본값
}
private boolean isValidTemperature(double temperature) {
return temperature >= -40.0 && temperature <= 80.0;
}
}
package com.sensor.bridge;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.jnrsmcu.sdk.netdevice.RSServer;
import com.jnrsmcu.sdk.netdevice.IDataListener;
import com.jnrsmcu.sdk.netdevice.NodeData;
import com.jnrsmcu.sdk.netdevice.RealTimeData;
import com.jnrsmcu.sdk.netdevice.LoginData;
import com.jnrsmcu.sdk.netdevice.StoreData;
import com.jnrsmcu.sdk.netdevice.TelecontrolAck;
import com.jnrsmcu.sdk.netdevice.TimmingAck;
import com.jnrsmcu.sdk.netdevice.ParamIdsData;
import com.jnrsmcu.sdk.netdevice.ParamData;
import com.jnrsmcu.sdk.netdevice.WriteParamAck;
import com.jnrsmcu.sdk.netdevice.TransDataAck;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
public class SensorBridge {
private static final Logger logger = LoggerFactory.getLogger(SensorBridge.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final OkHttpClient httpClient = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
private RSServer rsServer;
private String goServerUrl;
private String paramDatPath;
private boolean isRunning = false;
// 온도 필터링을 위한 이전 온도 값 저장
private double previousTemperature = 0.0;
// 온도 파싱 시스템
private final TemperatureParser temperatureParser;
public SensorBridge() {
// 온도 파서 초기화
this.temperatureParser = new TemperatureParser();
logger.info("TemperatureParser 초기화 완료");
}
public static void main(String[] args) {
SensorBridge bridge = new SensorBridge();
bridge.start();
}
public void start() {
try {
loadConfiguration();
initializeRSServer();
isRunning = true;
logger.info("Sensor Bridge started successfully");
// 서버가 종료될 때까지 대기
Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
while (isRunning) {
Thread.sleep(1000);
}
} catch (Exception e) {
logger.error("Failed to start Sensor Bridge", e);
System.exit(1);
}
}
public void stop() {
isRunning = false;
if (rsServer != null) {
rsServer.stop();
}
logger.info("Sensor Bridge stopped");
}
private void loadConfiguration() {
Properties props = new Properties();
try {
props.load(getClass().getClassLoader().getResourceAsStream("application.properties"));
} catch (IOException e) {
logger.warn("Could not load application.properties, using default values");
}
// 환경변수 우선, 그 다음 properties 파일, 마지막 기본값
goServerUrl = System.getenv("GO_SERVER_URL");
if (goServerUrl == null || goServerUrl.isEmpty()) {
goServerUrl = props.getProperty("go.server.url", "http://localhost:8080");
}
paramDatPath = props.getProperty("rs.server.param.file", "JavaSDKV2.2.2/param.dat");
logger.info("Go server URL: {}", goServerUrl);
logger.info("Param.dat path: {}", paramDatPath);
}
private void initializeRSServer() {
try {
logger.info("Loading param.dat from: {}", paramDatPath);
// RSServer 초기화 (포트 8020 사용)
rsServer = RSServer.Initiate(8020, paramDatPath);
// 데이터 리스너 설정
rsServer.addDataListener(new IDataListener() {
@Override
public void receiveRealtimeData(RealTimeData realTimeData) {
logger.info("Received realtime data");
// 실시간 데이터에서 NodeData 추출하여 처리
if (realTimeData != null) {
processRealTimeData(realTimeData);
}
}
@Override
public void receiveLoginData(LoginData loginData) {
logger.info("Received login data");
// 로그인 데이터 처리
}
@Override
public void receiveStoreData(StoreData storeData) {
logger.info("Received store data");
// 저장 데이터 처리
}
@Override
public void receiveTelecontrolAck(TelecontrolAck telecontrolAck) {
logger.info("Received telecontrol ack");
// 원격 제어 응답 처리
}
@Override
public void receiveTimmingAck(TimmingAck timmingAck) {
logger.info("Received timming ack");
// 타이밍 응답 처리
}
@Override
public void receiveParamIds(ParamIdsData paramIdsData) {
logger.info("Received param ids");
// 파라미터 ID 목록 처리
}
@Override
public void receiveParam(ParamData paramData) {
logger.info("Received param data");
// 파라미터 데이터 처리
}
@Override
public void receiveWriteParamAck(WriteParamAck writeParamAck) {
logger.info("Received write param ack");
// 파라미터 쓰기 응답 처리
}
@Override
public void receiveTransDataAck(TransDataAck transDataAck) {
logger.info("Received trans data ack");
// 데이터 전송 응답 처리
}
});
rsServer.start();
logger.info("RS Server started successfully");
} catch (Exception e) {
logger.error("Failed to initialize RS Server", e);
throw new RuntimeException("Failed to initialize RS Server", e);
}
}
private void processRealTimeData(RealTimeData realTimeData) {
try {
// RealTimeData 전체 정보 로깅
logger.info("Raw RealTimeData: deviceId={}, lng={}, lat={}, coordinateType={}, relayStatus={}, nodeCount={}",
realTimeData.getDeviceId(), realTimeData.getLng(), realTimeData.getLat(),
realTimeData.getCoordinateType(), realTimeData.getRelayStatus(),
realTimeData.getNodeList() != null ? realTimeData.getNodeList().size() : 0);
// RealTimeData에서 센서 데이터 추출
ExtendedSensorData extendedSensorData = new ExtendedSensorData();
// 기본 정보 설정
extendedSensorData.setDeviceId("sensor-" + realTimeData.getDeviceId());
// NodeList에서 첫 번째 노드의 ID 사용
int nodeId = 0;
if (realTimeData.getNodeList() != null && !realTimeData.getNodeList().isEmpty()) {
nodeId = realTimeData.getNodeList().get(0).getNodeId();
}
extendedSensorData.setNodeId(nodeId);
extendedSensorData.setRecordedTime(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
if (realTimeData.getNodeList() != null && !realTimeData.getNodeList().isEmpty()) {
NodeData firstNode = realTimeData.getNodeList().get(0);
// 원본 데이터 로깅
logger.info("Raw NodeData: nodeId={}, tem={}, hum={}, floatValue={}, signedInt32Value={}, unsignedInt32Value={}, recordTime={}",
firstNode.getNodeId(), firstNode.getTem(), firstNode.getHum(),
firstNode.getFloatValue(), firstNode.getSignedInt32Value(),
firstNode.getUnSignedInt32Value(), firstNode.getRecordTime());
// 원시 데이터 저장
double rawTem = firstNode.getTem();
double rawHum = firstNode.getHum();
double floatValue = firstNode.getFloatValue();
int signedInt32Value = firstNode.getSignedInt32Value();
long unsignedInt32Value = firstNode.getUnSignedInt32Value();
// 원시 데이터 설정
extendedSensorData.setRawTem(rawTem);
extendedSensorData.setRawHum(rawHum);
extendedSensorData.setFloatValue(floatValue);
extendedSensorData.setSignedInt32Value(signedInt32Value);
extendedSensorData.setUnsignedInt32Value(unsignedInt32Value);
// 다중 센서 데이터 파싱
// 온도 파싱 - 새로운 파싱 시스템 사용
double temperature = temperatureParser.parseTemperature(
new SensorData(rawTem, floatValue, signedInt32Value, unsignedInt32Value),
null // 센서 타입 자동 감지
);
// 온도 유효성 검증 (5도 제한 로직 제거)
if (temperature < -40.0 || temperature > 80.0) {
logger.warn("Temperature out of valid range: {}°C, using default value", temperature);
temperature = 25.0; // 기본값 사용
}
extendedSensorData.setTemperature(temperature);
previousTemperature = temperature;
logger.info("Temperature parsed: rawTem={}, floatValue={}, signedInt32Value={} -> {}°C",
rawTem, floatValue, signedInt32Value, temperature);
// 습도 파싱 (클라우드 대시보드 기준)
double humidity = SensorDataParser.parseHumidity(rawHum);
extendedSensorData.setHumidity(humidity);
// PM10 파싱
double pm10 = SensorDataParser.parsePM10(floatValue, signedInt32Value);
extendedSensorData.setPm10(pm10);
// PM2.5 파싱
double pm25 = SensorDataParser.parsePM25(floatValue, signedInt32Value);
extendedSensorData.setPm25(pm25);
// 기압 파싱
double pressure = SensorDataParser.parsePressure(signedInt32Value);
extendedSensorData.setPressure(pressure);
// 조도 파싱
double illumination = SensorDataParser.parseIllumination(floatValue, unsignedInt32Value);
extendedSensorData.setIllumination(illumination);
// TVOC 파싱
double tvoc = SensorDataParser.parseTVOC(floatValue, signedInt32Value);
extendedSensorData.setTvoc(tvoc);
// CO2 파싱
double co2 = SensorDataParser.parseCO2(floatValue, signedInt32Value);
extendedSensorData.setCo2(co2);
// O2 파싱
double o2 = SensorDataParser.parseO2(floatValue, unsignedInt32Value);
extendedSensorData.setO2(o2);
// CO 파싱
double co = SensorDataParser.parseCO(floatValue, signedInt32Value);
extendedSensorData.setCo(co);
}
// GPS 데이터 설정
extendedSensorData.setLongitude(realTimeData.getLng());
extendedSensorData.setLatitude(realTimeData.getLat());
logger.info("Processed extended sensor data: deviceId={}, nodeId={}, temp={}, humidity={}, pm10={}, pm25={}, pressure={}, illumination={}, tvoc={}, co2={}, o2={}, co={}, lng={}, lat={}",
extendedSensorData.getDeviceId(), extendedSensorData.getNodeId(),
extendedSensorData.getTemperature(), extendedSensorData.getHumidity(),
extendedSensorData.getPm10(), extendedSensorData.getPm25(),
extendedSensorData.getPressure(), extendedSensorData.getIllumination(),
extendedSensorData.getTvoc(), extendedSensorData.getCo2(),
extendedSensorData.getO2(), extendedSensorData.getCo(),
extendedSensorData.getLongitude(), extendedSensorData.getLatitude());
sendExtendedDataToGoServer(extendedSensorData);
} catch (Exception e) {
logger.error("Error processing realtime data", e);
}
}
private void sendExtendedDataToGoServer(ExtendedSensorData extendedSensorData) {
try {
String json = objectMapper.writeValueAsString(extendedSensorData);
logger.info("Sending extended data to Go server: {}", json);
RequestBody body = RequestBody.create(
json,
MediaType.get("application/json; charset=utf-8")
);
Request request = new Request.Builder()
.url(goServerUrl + "/api/sensor/extended-data")
.post(body)
.build();
try (Response response = httpClient.newCall(request).execute()) {
if (response.isSuccessful()) {
logger.info("Extended data sent successfully to Go server");
} else {
logger.error("Failed to send extended data to Go server. Response code: {}, Body: {}",
response.code(), response.body() != null ? response.body().string() : "null");
}
}
} catch (Exception e) {
logger.error("Error sending extended data to Go server", e);
}
}
}
\ No newline at end of file
package com.sensor.bridge;
/**
* 센서 데이터를 담는 클래스
* 온도 파싱에 필요한 모든 데이터 필드를 포함
*/
public class SensorData {
private final double rawTem;
private final double floatValue;
private final int signedInt32Value;
private final long unsignedInt32Value;
public SensorData(double rawTem, double floatValue, int signedInt32Value, long unsignedInt32Value) {
this.rawTem = rawTem;
this.floatValue = floatValue;
this.signedInt32Value = signedInt32Value;
this.unsignedInt32Value = unsignedInt32Value;
}
// Getters
public double getRawTem() {
return rawTem;
}
public double getFloatValue() {
return floatValue;
}
public int getSignedInt32Value() {
return signedInt32Value;
}
public long getUnsignedInt32Value() {
return unsignedInt32Value;
}
@Override
public String toString() {
return String.format("SensorData{rawTem=%.2f, floatValue=%.2f, signedInt32Value=%d, unsignedInt32Value=%d}",
rawTem, floatValue, signedInt32Value, unsignedInt32Value);
}
}
package com.sensor.bridge;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 센서 데이터 파싱 로직을 담당하는 클래스
* 다양한 센서 타입의 데이터를 파싱하고 변환
*/
public class SensorDataParser {
private static final Logger logger = LoggerFactory.getLogger(SensorDataParser.class);
// 센서별 유효 범위 정의
private static final double PM10_MIN = 0.0;
private static final double PM10_MAX = 1000.0;
private static final double PM25_MIN = 0.0;
private static final double PM25_MAX = 500.0;
private static final double PRESSURE_MIN = 800.0;
private static final double PRESSURE_MAX = 1200.0;
private static final double ILLUMINATION_MIN = 0.0;
private static final double ILLUMINATION_MAX = 100000.0;
private static final double TVOC_MIN = 0.0;
private static final double TVOC_MAX = 10000.0;
private static final double CO2_MIN = 300.0;
private static final double CO2_MAX = 5000.0;
private static final double O2_MIN = 15.0;
private static final double O2_MAX = 25.0;
private static final double CO_MIN = 0.0;
private static final double CO_MAX = 100.0;
private static final double TEMPERATURE_MIN = -40.0;
private static final double TEMPERATURE_MAX = 80.0;
private static final double HUMIDITY_MIN = 0.0;
private static final double HUMIDITY_MAX = 100.0;
/**
* 센서 값 검증
* @param value 검증할 값
* @param min 최소값
* @param max 최대값
* @param sensorName 센서 이름 (로깅용)
* @return 유효한 값인지 여부
*/
private static boolean isValidValue(double value, double min, double max, String sensorName) {
// NaN, 무한대, 매우 작은 값(e-40 등) 체크
if (Double.isNaN(value) || Double.isInfinite(value) || Math.abs(value) < 1e-10) {
logger.warn("{}: Invalid value detected: {}", sensorName, value);
return false;
}
// 범위 체크
if (value < min || value > max) {
logger.warn("{}: Value out of range: {} (valid range: {} - {})", sensorName, value, min, max);
return false;
}
return true;
}
/**
* PM10 미세먼지 데이터 파싱 (개선된 버전)
* @param floatValue float 값
* @param signedInt32Value signed int32 값
* @return 파싱된 PM10 값
*/
public static double parsePM10(double floatValue, int signedInt32Value) {
// floatValue가 유효한 범위인 경우 (PM10 전용 범위 체크)
if (isValidValue(floatValue, PM10_MIN, PM10_MAX, "PM10")) {
logger.debug("PM10 from floatValue: {}", floatValue);
return floatValue;
}
// signedInt32Value에서 추출 (하위 16비트)
double pm10 = (signedInt32Value & 0xFFFF) / 100.0;
// 추출된 값 검증
if (isValidValue(pm10, PM10_MIN, PM10_MAX, "PM10")) {
logger.debug("PM10 from signedInt32Value: {} -> {}", signedInt32Value, pm10);
return pm10;
}
logger.warn("PM10: All parsing methods failed, using default value");
return 0.0;
}
/**
* PM2.5 미세먼지 데이터 파싱 (개선된 버전)
* @param floatValue float 값
* @param signedInt32Value signed int32 값
* @return 파싱된 PM2.5 값
*/
public static double parsePM25(double floatValue, int signedInt32Value) {
// floatValue가 유효한 범위인 경우 (PM2.5 전용 범위 체크)
if (isValidValue(floatValue, PM25_MIN, PM25_MAX, "PM2.5")) {
logger.debug("PM2.5 from floatValue: {}", floatValue);
return floatValue;
}
// signedInt32Value에서 추출 (상위 16비트)
double pm25 = ((signedInt32Value >> 16) & 0xFFFF) / 100.0;
// 추출된 값 검증
if (isValidValue(pm25, PM25_MIN, PM25_MAX, "PM2.5")) {
logger.debug("PM2.5 from signedInt32Value: {} -> {}", signedInt32Value, pm25);
return pm25;
}
logger.warn("PM2.5: All parsing methods failed, using default value");
return 0.0;
}
/**
* 기압 데이터 파싱 (개선된 버전)
* @param signedInt32Value signed int32 값
* @return 파싱된 기압 값 (hPa)
*/
public static double parsePressure(int signedInt32Value) {
// 상위 16비트에서 기압 데이터 추출
int pressureRaw = (signedInt32Value >> 16) & 0xFFFF;
double pressure = pressureRaw / 10.0 + 800.0; // 기본값 800hPa에서 시작
// 추출된 값 검증
if (isValidValue(pressure, PRESSURE_MIN, PRESSURE_MAX, "Pressure")) {
logger.debug("Pressure from signedInt32Value: {} -> {} hPa", signedInt32Value, pressure);
return pressure;
}
logger.warn("Pressure: Invalid value, using default value");
return 1013.25; // 표준 대기압
}
/**
* 조도 데이터 파싱 (개선된 버전)
* @param floatValue float 값
* @param unsignedInt32Value unsigned int32 값
* @return 파싱된 조도 값 (lux)
*/
public static double parseIllumination(double floatValue, long unsignedInt32Value) {
// floatValue가 유효한 범위인 경우 (조도 전용 범위 체크)
if (isValidValue(floatValue, ILLUMINATION_MIN, ILLUMINATION_MAX, "Illumination")) {
logger.debug("Illumination from floatValue: {} lux", floatValue);
return floatValue;
}
// unsignedInt32Value에서 추출
double illumination = (unsignedInt32Value & 0xFFFF) * 10.0;
// 추출된 값 검증
if (isValidValue(illumination, ILLUMINATION_MIN, ILLUMINATION_MAX, "Illumination")) {
logger.debug("Illumination from unsignedInt32Value: {} -> {} lux", unsignedInt32Value, illumination);
return illumination;
}
logger.warn("Illumination: All parsing methods failed, using default value");
return 0.0;
}
/**
* TVOC (Total Volatile Organic Compounds) 데이터 파싱 (개선된 버전)
* @param floatValue float 값
* @param signedInt32Value signed int32 값
* @return 파싱된 TVOC 값 (ppb)
*/
public static double parseTVOC(double floatValue, int signedInt32Value) {
// floatValue가 유효한 범위인 경우 (TVOC 전용 범위 체크)
if (isValidValue(floatValue, TVOC_MIN, TVOC_MAX, "TVOC")) {
logger.debug("TVOC from floatValue: {} ppb", floatValue);
return floatValue;
}
// signedInt32Value에서 추출 (하위 16비트)
double tvoc = (signedInt32Value & 0xFFFF);
// 추출된 값 검증
if (isValidValue(tvoc, TVOC_MIN, TVOC_MAX, "TVOC")) {
logger.debug("TVOC from signedInt32Value: {} -> {} ppb", signedInt32Value, tvoc);
return tvoc;
}
logger.warn("TVOC: All parsing methods failed, using default value");
return 0.0;
}
/**
* CO2 데이터 파싱 (개선된 버전)
* @param floatValue float 값
* @param signedInt32Value signed int32 값
* @return 파싱된 CO2 값 (ppm)
*/
public static double parseCO2(double floatValue, int signedInt32Value) {
// floatValue가 유효한 범위인 경우 (CO2 전용 범위 체크)
if (isValidValue(floatValue, CO2_MIN, CO2_MAX, "CO2")) {
logger.debug("CO2 from floatValue: {} ppm", floatValue);
return floatValue;
}
// signedInt32Value에서 추출 (상위 16비트)
double co2 = ((signedInt32Value >> 16) & 0xFFFF) + 400; // 기본값 400ppm에서 시작
// 추출된 값 검증
if (isValidValue(co2, CO2_MIN, CO2_MAX, "CO2")) {
logger.debug("CO2 from signedInt32Value: {} -> {} ppm", signedInt32Value, co2);
return co2;
}
logger.warn("CO2: All parsing methods failed, using default value");
return 400.0; // 대기 중 CO2 농도
}
/**
* O2 데이터 파싱 (개선된 버전)
* @param floatValue float 값
* @param unsignedInt32Value unsigned int32 값
* @return 파싱된 O2 값 (%)
*/
public static double parseO2(double floatValue, long unsignedInt32Value) {
// floatValue가 유효한 범위인 경우 (O2 전용 범위 체크)
if (isValidValue(floatValue, O2_MIN, O2_MAX, "O2")) {
logger.debug("O2 from floatValue: {}%", floatValue);
return floatValue;
}
// unsignedInt32Value에서 추출 (상위 16비트)
double o2 = ((unsignedInt32Value >> 16) & 0xFFFF) / 100.0;
// 추출된 값 검증
if (isValidValue(o2, O2_MIN, O2_MAX, "O2")) {
logger.debug("O2 from unsignedInt32Value: {} -> {}%", unsignedInt32Value, o2);
return o2;
}
logger.warn("O2: All parsing methods failed, using default value");
return 20.9; // 대기 중 산소 농도
}
/**
* CO 데이터 파싱 (개선된 버전)
* @param floatValue float 값
* @param signedInt32Value signed int32 값
* @return 파싱된 CO 값 (ppm)
*/
public static double parseCO(double floatValue, int signedInt32Value) {
// floatValue가 유효한 범위인 경우 (CO 전용 범위 체크)
if (isValidValue(floatValue, CO_MIN, CO_MAX, "CO")) {
logger.debug("CO from floatValue: {} ppm", floatValue);
return floatValue;
}
// signedInt32Value에서 추출 (하위 16비트)
double co = (signedInt32Value & 0xFFFF) / 100.0;
// 추출된 값 검증
if (isValidValue(co, CO_MIN, CO_MAX, "CO")) {
logger.debug("CO from signedInt32Value: {} -> {} ppm", signedInt32Value, co);
return co;
}
logger.warn("CO: All parsing methods failed, using default value");
return 0.0;
}
/**
* 온도 데이터 파싱 (개선된 버전 - 5도 제한 로직 제거)
* @param rawTem 원시 온도 값
* @param floatValue float 값
* @param signedInt32Value signed int32 값
* @return 파싱된 온도 값 (°C)
*/
public static double parseTemperature(double rawTem, double floatValue, int signedInt32Value) {
// 1. signedInt32Value가 실제 온도인 경우 (대시보드: 29.1°C)
if (signedInt32Value > 500000 && signedInt32Value < 600000) {
// 524306을 온도로 변환: 524306 / 18000 ≈ 29.1°C
double temp = signedInt32Value / 18000.0;
if (isValidValue(temp, TEMPERATURE_MIN, TEMPERATURE_MAX, "Temperature")) {
logger.debug("Temperature from signedInt32Value (dashboard match): {} -> {}°C", signedInt32Value, temp);
return temp;
}
}
// 2. rawTem이 0.1도 단위로 저장된 경우 (기존 rawTem * 10 공식)
if (isValidValue(rawTem, 0, 50, "Raw Temperature")) {
double temp = rawTem * 10;
if (isValidValue(temp, TEMPERATURE_MIN, TEMPERATURE_MAX, "Temperature")) {
logger.debug("Temperature conversion (rawTem * 10): {} -> {}°C", rawTem, temp);
return temp;
}
}
// 3. floatValue가 실제 온도인 경우
if (isValidValue(floatValue, TEMPERATURE_MIN, TEMPERATURE_MAX, "Temperature")) {
logger.debug("Using floatValue as temperature: {}°C", floatValue);
return floatValue;
}
// 4. signedInt32Value에서 다른 패턴으로 온도 추출 시도
if (signedInt32Value != 0) {
// 다양한 스케일 팩터로 시도
double[] scaleFactors = {1.0, 0.1, 0.01, 10.0, 100.0};
for (double scale : scaleFactors) {
double temp = signedInt32Value * scale;
if (isValidValue(temp, TEMPERATURE_MIN, TEMPERATURE_MAX, "Temperature")) {
logger.debug("Temperature from signedInt32Value with scale {}: {} -> {}°C", scale, signedInt32Value, temp);
return temp;
}
}
}
// 5. 모든 방법 실패 시 기본값 반환
logger.warn("Temperature: All parsing methods failed, using default value 25.0°C");
logger.debug("Failed values - rawTem: {}, floatValue: {}, signedInt32Value: {}",
rawTem, floatValue, signedInt32Value);
return 25.0;
}
/**
* 습도 데이터 파싱 (클라우드 대시보드 기준, 개선된 버전)
* @param rawHum 원시 습도 값
* @return 파싱된 습도 값 (%)
*/
public static double parseHumidity(double rawHum) {
// 대시보드 습도 35.0%에 맞춰 변환
if (isValidValue(rawHum, 0, 10, "Raw Humidity")) {
// 0.8 -> 35.0% 변환: 0.8 * 43.75 ≈ 35.0%
double humidity = rawHum * 43.75;
if (isValidValue(humidity, HUMIDITY_MIN, HUMIDITY_MAX, "Humidity")) {
logger.debug("Humidity conversion (dashboard match): {} -> {}%", rawHum, humidity);
return humidity;
}
} else if (isValidValue(rawHum, 10, 100, "Humidity")) {
// 이미 적절한 범위인 경우
logger.debug("Humidity already in range: {}", rawHum);
return rawHum;
}
// 기본값
logger.warn("Humidity: All parsing methods failed, using default value");
return 60.0;
}
}
\ No newline at end of file
package com.sensor.bridge;
/**
* 센서별 온도 파싱을 위한 인터페이스
* 각 센서 타입별로 특화된 파싱 로직을 구현
*/
public interface SensorParser {
/**
* 온도 데이터 파싱
* @param data 센서 데이터
* @param paramItem 파라미터 정보
* @return 파싱된 온도 값 (°C)
*/
double parseTemperature(SensorData data, ParamItem paramItem);
/**
* 센서 데이터 유효성 검증
* @param data 센서 데이터
* @return 유효한 데이터인지 여부
*/
boolean isValid(SensorData data);
/**
* 센서 타입 반환
* @return 센서 타입
*/
String getSensorType();
/**
* 파싱 품질 점수 반환 (0.0 ~ 1.0)
* @param data 센서 데이터
* @return 파싱 품질 점수
*/
double getParsingQuality(SensorData data);
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment