Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/docker-build-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
branches:
- main
- develop

jobs:
test-and-deploy:
Expand Down Expand Up @@ -39,11 +40,22 @@ jobs:
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1

- name: Set IMAGE_TAG based on branch
id: set-tag
run: |
if [[ "${{ github.base_ref }}" == "main" ]]; then
echo "tag=prod-latest" >> $GITHUB_OUTPUT
elif [[ "${{ github.base_ref }}" == "develop" ]]; then
echo "tag=dev-latest" >> $GITHUB_OUTPUT
else
echo "tag=unknown" >> $GITHUB_OUTPUT
fi

- name: Build, tag, and push image to ECR
env:
ECR_REGISTRY: ${{ secrets.AWS_ECR_REGISTRY }}
ECR_REPOSITORY: streamlit-app
IMAGE_TAG: streamlit-latest
IMAGE_TAG: ${{ steps.set-tag.outputs.tag }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
4 changes: 3 additions & 1 deletion service/sensor/sensorTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
time.sleep(1) # 연결 대기

flag = 0
max_cnt = 30 #30번번 # 몇 번 받아올건지
max_cnt = 50 #30번번 # 몇 번 받아올건지
# query = "INSERT INTO ua10_table (temperature, humidity) VALUES (%s, %s)"

publisher = AwsMQTT()
Expand Down Expand Up @@ -56,6 +56,8 @@
stm, tmp, hmd = line.split(",")
payload = json.dumps({
"id": "UA10H-CHS-24060894",
"equip_id": "20250507165750-827",
"zone_id": "20250507165750-827",
"type": "온습도",
"temperature": float(tmp),
"humidity": float(hmd)
Expand Down
40 changes: 40 additions & 0 deletions service/simulatelogic/ContinuousSimulatorMixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# continuous_simulator.py
import random
from scipy.stats import truncnorm

class ContinuousSimulatorMixin:
"""평균 회귀 + 작은 노이즈 + 희박한 이상치 발생 로직을 공유"""
# ── 하위 클래스가 오버라이드할 매개변수 ───────────────────
SENSOR_TYPE = None # "temp" / "humid" / ...
MU = None # 평균
SIGMA = None # 표준편차
LOWER = None # 최소 허용값
UPPER = None # 최대 허용값
OUTLIER_P = 0.03 # 기본 이상치 확률 3%
DRIFT_THETA = 0.1 # 평균 회귀 강도 ** 데이터 정상범위 유지하는 중요 데이터터
SMALL_SIGMA_RATIO = 0.1 # 정상 구간 변동폭 (σ의 10 %)

# ── 내부 상태 초기화 ─────────────────────────────────────
def _reset_state(self):
a, b = (self.LOWER - self.MU) / self.SIGMA, (self.UPPER - self.MU) / self.SIGMA
first = truncnorm.rvs(a, b, loc=self.MU, scale=self.SIGMA/3)
self.prev_val = round(first, 2)

# ── 핵심 데이터 생성 로직 ─────────────────────────────────
def _generate_continuous_val(self):
if not hasattr(self, "prev_val"):
self._reset_state()

# 1) 평균 회귀 + 작은 노이즈
mean_revert = self.MU + (self.prev_val - self.MU) * (1 - self.DRIFT_THETA)
small_sigma = self.SIGMA * self.SMALL_SIGMA_RATIO
val = random.gauss(mean_revert, small_sigma)

# 2) 이상치
if random.random() < self.OUTLIER_P:
val = random.gauss(self.MU, self.SIGMA)

# 3) 범위 클램프 & 저장
val = round(max(self.LOWER, min(self.UPPER, val)), 2)
self.prev_val = val
return val
27 changes: 17 additions & 10 deletions service/simulation/CurrentSimulator.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
from .SimulatorInterface2 import SimulatorInterface2
from simulate_type.simulate_list import generate_current_data
from scipy.stats import truncnorm
from service.simulatelogic.ContinuousSimulatorMixin import ContinuousSimulatorMixin

class CurrentSimulator(ContinuousSimulatorMixin,SimulatorInterface2):
# 타입별 시뮬레이터 세팅
SENSOR_TYPE = "current" # 센서 타입
# MU, SIGMA = 62.51, 33.76
MU, SIGMA = 5, 30 # 평균치 , 표준 편차
LOWER, UPPER = 1,50 # 측정 범위
OUTLIER_P = 0.10 # 10 % 확률로 경보 값 생성

class CurrentSimulator(SimulatorInterface2):
def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_count:int = 10, conn=None):
# 시뮬레이터에서 공통적으로 사용하는 속성
super().__init__(
Expand All @@ -20,12 +26,12 @@ def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_co
self.shadow_desired_topic_name = f"$aws/things/Sensor/shadow/name/{self.sensor_id}/update/desired"
self.topic_name = f"sensor/{zone_id}/{equip_id}/{self.sensor_id}/{self.type}"
self.target_current = None # 초기값 설정(shadow 용)
self.mu = 62.51
self.sigma = 33.76
lower = 0
upper = self.mu + 3 * self.sigma
self.a = (lower - self.mu) / self.sigma
self.b = (upper - self.mu) / self.sigma
# self.mu = 62.51
# self.sigma = 33.76
# lower = 0
# upper = self.mu + 3 * self.sigma
# self.a = (lower - self.mu) / self.sigma
# self.b = (upper - self.mu) / self.sigma


# 데이터 생성 로직 정의
Expand All @@ -36,7 +42,8 @@ def _generate_data(self) -> dict:
"sensorId": self.sensor_id,
"sensorType": self.type,
# "val": round(random.uniform(0.1 + self.idx, 10.0 + self.idx), 2)
"val": round(truncnorm.rvs(self.a, self.b, loc=self.mu, scale=self.sigma), 2) # 0: 7, 1: 7이상, 2: 30 이상 최소값은 0
"val": self._generate_continuous_val()
# 0: 7, 1: 7이상, 2: 30 이상 최소값은 0
}

################################################
Expand Down
37 changes: 21 additions & 16 deletions service/simulation/DustSimulator.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from .SimulatorInterface2 import SimulatorInterface2
import random
from scipy.stats import truncnorm
from service.simulatelogic.ContinuousSimulatorMixin import ContinuousSimulatorMixin

class DustSimulator(ContinuousSimulatorMixin,SimulatorInterface2):
# dust_simulator.py (Mixin 상수 부분만)
SENSOR_TYPE = "dust" # ㎍/㎥
MU, SIGMA = 50, 25 # 평균 50 ㎍/㎥, σ = 25
LOWER, UPPER = 0, 300 # 0 ‒ 300 ㎍/㎥ 범위
# SMALL_SIGMA_RATIO = 0.10 # 정상 변동폭 = σ의 10 %(≈ ±2.5)

class DustSimulator(SimulatorInterface2):
def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_count:int = 10, conn=None):
# 시뮬레이터에서 공통적으로 사용하는 속성
super().__init__(
Expand All @@ -21,13 +26,13 @@ def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_co
self.topic_name = f"sensor/{zone_id}/{equip_id}/{self.sensor_id}/{self.type}"
self.target_current = None # 초기값 설정(shadow 용)

self.mu = 180 # 평균 미세먼지 수치
self.sigma = 60 # 표준편차
self.lower = 0
self.upper = self.mu + 3 * self.sigma
# self.mu = 180 # 평균 미세먼지 수치
# self.sigma = 60 # 표준편차
# self.lower = 0
# self.upper = self.mu + 3 * self.sigma

self.a = (self.lower - self.mu) / self.sigma
self.b = (self.upper - self.mu) / self.sigma
# self.a = (self.lower - self.mu) / self.sigma
# self.b = (self.upper - self.mu) / self.sigma

# 데이터 생성 로직 정의
def _generate_data(self) -> dict:
Expand All @@ -36,7 +41,7 @@ def _generate_data(self) -> dict:
"equipId": self.equip_id,
"sensorId": self.sensor_id,
"sensorType": self.type,
"val": round(truncnorm.rvs(self.a, self.b, loc=self.mu, scale=self.sigma), 2)
"val": self._generate_continuous_val()
}

################################################
Expand All @@ -46,11 +51,11 @@ def _generate_data(self) -> dict:
def _apply_desired_state(self, desired_state):
"""
Shadow의 desired 상태를 받아서 센서에 적용
예) {"target_Vibration": 25.0} 이런 명령을 받아 적용
예) {"target_Dust": 25.0} 이런 명령을 받아 적용
"""
target_current = desired_state.get("target_current")
if target_current is not None:
self.target_current = target_current
print(f"Desired state applied: {self.sensor_id} - Target Current: {self.target_current}")
target_dust = desired_state.get("target_dust")
if target_dust is not None:
self.target_dust = target_dust
print(f"Desired state applied: {self.sensor_id} - Target Dust: {self.target_dust}")
else:
print(f"No target current provided for {self.sensor_id}.")
print(f"No target dust provided for {self.sensor_id}.")
125 changes: 66 additions & 59 deletions service/simulation/HumiditySimulator.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,67 @@
from .SimulatorInterface2 import SimulatorInterface2
import random

class HumiditySimulator(SimulatorInterface2):
def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_count:int = 10, conn=None):
#########################################
# 시뮬레이터에서 공통적으로 사용하는 속성
#########################################
super().__init__(
idx=idx,
zone_id=zone_id,
equip_id=equip_id,
interval=interval,
msg_count=msg_count,
conn=conn
)

#########################################
# 시뮬레이터 마다 개별적으로 사용하는 속성(토픽, 수집 데이터 초기값)
#########################################
self.sensor_id = f"UA10H-HUM-2406089{idx}" # 센서 ID
self.type = "humid" # 센서 타입
# shadow 등록용 토픽
self.shadow_regist_topic_name = f"$aws/things/Sensor/shadow/name/{self.sensor_id}/update"
# shadow 제어 명령 구독용 토픽
self.shadow_desired_topic_name = f"$aws/things/Sensor/shadow/name/{self.sensor_id}/update/desired"
# 센서 데이터 publish용 토픽
self.topic_name = f"sensor/{zone_id}/{equip_id}/{self.sensor_id}/{self.type}"
self.target_temperature = None # 초기값 설정(shadow 용)

################################################z
# 데이터 생성 로직을 정의 (시뮬레이터 마다 다르게 구현)
# 예) 온도, 습도, 진동, 전류 등등
################################################
def _generate_data(self) -> dict:
""" 데이터 생성 메서드 """
return {
"zoneId": self.zone_id,
"equipId": self.equip_id,
"sensorId": self.sensor_id,
"sensorType": self.type,
"val": round(random.gauss(mu = 11.68, sigma = 29.38), 2) # 0: 60이하,1: 60초과, 2: 80 초과과
}

################################################
# 제어 로직을 정의 ( shadow의 desired 상태를 구독하여 제어하는 로직을 구현할 예정)
################################################
def _apply_desired_state(self, desired_state):
"""
Shadow의 desired 상태를 받아서 센서에 적용
예) {"target_humidity": 25.0} 이런 명령을 받아 적용
"""
target_humidity = desired_state.get("target_humidity")
if target_humidity is not None:
self.target_humidity = target_humidity
print(f"Desired state applied: {self.sensor_id} - Target humidity: {self.target_humidity}")
else:
print(f"No target humidity provided for {self.sensor_id}.")

from .SimulatorInterface2 import SimulatorInterface2
import random
from service.simulatelogic.ContinuousSimulatorMixin import ContinuousSimulatorMixin

class HumiditySimulator(ContinuousSimulatorMixin,SimulatorInterface2):
# 타입별 시뮬레이터 세팅
SENSOR_TYPE = "humid"
MU, SIGMA = 55, 15
LOWER, UPPER = 0, 100
OUTLIER_P = 0.1

def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_count:int = 10, conn=None):
#########################################
# 시뮬레이터에서 공통적으로 사용하는 속성
#########################################
super().__init__(
idx=idx,
zone_id=zone_id,
equip_id=equip_id,
interval=interval,
msg_count=msg_count,
conn=conn
)

#########################################
# 시뮬레이터 마다 개별적으로 사용하는 속성(토픽, 수집 데이터 초기값)
#########################################
self.sensor_id = f"UA10H-HUM-3406089{idx}" # 센서 ID
self.type = "humid" # 센서 타입
# shadow 등록용 토픽
self.shadow_regist_topic_name = f"$aws/things/Sensor/shadow/name/{self.sensor_id}/update"
# shadow 제어 명령 구독용 토픽
self.shadow_desired_topic_name = f"$aws/things/Sensor/shadow/name/{self.sensor_id}/update/desired"
# 센서 데이터 publish용 토픽
self.topic_name = f"sensor/{zone_id}/{equip_id}/{self.sensor_id}/{self.type}"
self.target_temperature = None # 초기값 설정(shadow 용)

################################################z
# 데이터 생성 로직을 정의 (시뮬레이터 마다 다르게 구현)
# 예) 온도, 습도, 진동, 전류 등등
################################################
def _generate_data(self) -> dict:
""" 데이터 생성 메서드 """
return {
"zoneId": self.zone_id,
"equipId": self.equip_id,
"sensorId": self.sensor_id,
"sensorType": self.type,
"val": self._generate_continuous_val()
}

################################################
# 제어 로직을 정의 ( shadow의 desired 상태를 구독하여 제어하는 로직을 구현할 예정)
################################################
def _apply_desired_state(self, desired_state):
"""
Shadow의 desired 상태를 받아서 센서에 적용
예) {"target_humidity": 25.0} 이런 명령을 받아 적용
"""
target_humidity = desired_state.get("target_humidity")
if target_humidity is not None:
self.target_humidity = target_humidity
print(f"Desired state applied: {self.sensor_id} - Target humidity: {self.target_humidity}")
else:
print(f"No target humidity provided for {self.sensor_id}.")


15 changes: 11 additions & 4 deletions service/simulation/SimulatorInterface2.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,13 @@ def _publish_loop(self):

try:
for _ in range(self.msg_count):
if self.stop_event.is_set():
break
self._publish_data()
time.sleep(self.interval)
try:
if self.stop_event.is_set():
break
self._publish_data()
time.sleep(self.interval)
except Exception as e:
print(f"Error in publish loop: {e}")
finally:
self._update_shadow(status="OFF")
########################################################################################
Expand Down Expand Up @@ -134,3 +137,7 @@ def generate_truncated_normal(self, mu: float, sigma: float, lower: float = None
a, b = (lower - mu) / sigma, (upper - mu) / sigma
value = truncnorm.rvs(a, b, loc=mu, scale=sigma)
return round(value, 2)

def _build_topic(self, zone_id, equip_id, sensor_id, sensor_type):
prefix = "zone" if zone_id == equip_id else "equip"
return f"sensor/{prefix}/{zone_id}/{equip_id}/{sensor_id}/{sensor_type}"
Loading
Loading