Skip to content
Closed
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: 11 additions & 5 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
"version": "0.2.0",
"configurations": [
{
"name": "Python: Module Debug",
"name": "Streamlit: Debug",
"type": "debugpy",
"request": "launch",
"module": "service.simulation.simulateTest", // 실행할 모듈 경로
"cwd": "${workspaceFolder}", // 작업 디렉토리
"console": "integratedTerminal", // 통합 터미널에서 실행
"justMyCode": false // 외부 라이브러리 코드도 디버깅
"module": "streamlit", // Streamlit 모듈 실행
"args": [
"run", "${workspaceFolder}/streamlit_app/app.py" // Streamlit 앱의 진입점
],
"console": "integratedTerminal", // 통합 터미널에서 실행
"cwd": "${workspaceFolder}", // 작업 디렉토리
"env": {
"PYTHONPATH": "${workspaceFolder}" // PYTHONPATH 설정
},
"justMyCode": false // 외부 라이브러리 코드도 디버깅
}
]
}
2 changes: 1 addition & 1 deletion mqtt_util/publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def publish(self , topic, payload, qos):
topic = topic,
payload = payload,
qos = qos)
print(f"Published: {payload} to topic: {topic}")
# print(f"Published: {payload} to topic: {topic}")

def subscribe(self, topic, qos, callback):
print(f"topic Subscribe: {topic} ")
Expand Down
Empty file added service/__init__.py
Empty file.
94 changes: 94 additions & 0 deletions service/sensor/RealSensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# real_sensor_publishers.py
import threading, serial, time, json
from awscrt import mqtt
from service.simulation.SimulatorInterface2 import SimulatorInterface2
from mqtt_util.publish import AwsMQTT

class RealSensor(SimulatorInterface2):
def __init__(self, idx, zone_id, equip_id, interval, msg_count, conn=None, stop_event=None):
super().__init__(idx, zone_id, equip_id, interval, msg_count, conn=conn)
self._is_publishing = False # 중복 실행 방지 플래그
self.stop_event = stop_event if stop_event else threading.Event()

# (1) 센서 고유 ID
self.sensor_id = f"UA10H-REAL-24060999"

# (2) shadow 토픽
self.shadow_regist_topic_name = f"$aws/things/Sensor/shadow/name/{self.sensor_id}/update"
self.shadow_desired_topic_name = f"$aws/things/Sensor/shadow/name/{self.sensor_id}/update/desired"

# (3) **온도·습도용 토픽 미리 생성**
self.topic_name_temp = self._build_topic(self.zone_id, self.equip_id, self.sensor_id, "temp")
self.topic_name_humid = self._build_topic(self.zone_id, self.equip_id, self.sensor_id, "humid")
# 시리얼 포트 설정
self.serial_port = 'COM3' # Windows COM 포트
self.baudrate = 9600 # 바우드

######## 오버라이딩은 하되 내용은 필요없는 메서드 ########
def _generate_data(self):
# RealSensor 에선 이 메서드 대신 _read_and_publish_loop 을 씁니다.
raise NotImplementedError

def _apply_desired_state(self, _):
# 아직 shadow 제어 명령 처리 필요 없으면 그냥 pass
pass
#################################################

def start_publishing(self):
"""실 센서용 읽기+퍼블리시 루프를 스레드로 돌립니다."""
# 이미 시작했으면 다시 만들지 않음
# if getattr(self, "_started", False):
# print("[RealSensor] already running – second call ignored")
# return None

if self._is_publishing:
print("[RealSensor] Publishing is already running. Ignoring second call.")
return None

self._is_publishing = True

thread = threading.Thread(target=self._read_and_publish_loop, daemon=False)
thread.start()
return thread

def _read_and_publish_loop(self):
try:
# 1) 시리얼 열고 대기
ser = serial.Serial(self.serial_port, self.baudrate, timeout=1)
time.sleep(2)

self.type = "real_sensor"
# 2) shadow ON
self._update_shadow("ON")
self._subscribe_to_shadow_desired()

# 3) msg_count 번만큼 읽어서 퍼블리시
for _ in range(self.msg_count):
# stop_event 확인 - 중지 요청 있으면 루프 종료
if self.stop_event.is_set():
print(f"[RealSensor] Stopping due to stop_event")
break

line = ser.readline().decode().strip()
if not line.startswith("STREAM"):
continue
_, tmp, hmd = line.split(",")
temperature = round(float(tmp), 2)
humidity = round(float(hmd), 2)

# → 여기만 교체
self.publish_value("temp", temperature)
self.publish_value("humid", humidity)

# print(f"[{time.strftime('%H:%M:%S')}] temp: {temperature}, humid: {humidity} {threading.current_thread().name}")
time.sleep(self.interval)

finally:
# ▼ 반드시 호출돼서 핸들을 반납
try:
ser.close()
except Exception:
pass # 이미 닫혔으면 무시
self._update_shadow("OFF")
self._is_publishing = False
# self._started = False # 다음 실행을 위해 플래그 해제
Empty file added service/sensor/__init__.py
Empty file.
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 = 15 #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
29 changes: 18 additions & 11 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 @@ -18,14 +24,14 @@ def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_co
self.type = "current" # 센서 타입
self.shadow_regist_topic_name = f"$aws/things/Sensor/shadow/name/{self.sensor_id}/update"
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.topic_name = self._build_topic(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
39 changes: 22 additions & 17 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 @@ -18,16 +23,16 @@ def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_co
self.type = "dust" # 센서 타입
self.shadow_regist_topic_name = f"$aws/things/Sensor/shadow/name/{self.sensor_id}/update"
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.topic_name = self._build_topic(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}.")
Loading
Loading