diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d48ad4e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +# Ignore Python cache files +*.pyc +__pycache__/ + +# Ignore environment files +.env + +# Ignore virtual environments +venv/ +.venv/ + +# Ignore other unnecessary files +*.log +*.tmp \ No newline at end of file diff --git a/.github/workflows/docker-build-develop.yml b/.github/workflows/docker-build-develop.yml new file mode 100644 index 0000000..e155661 --- /dev/null +++ b/.github/workflows/docker-build-develop.yml @@ -0,0 +1,49 @@ +name: Test and Build Python Image to ECR + +on: + pull_request: + branches: + - main + +jobs: + test-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run Streamlit sanity check + run: | + streamlit --version + python -m pytest || echo "Tests not configured, skipping..." + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ap-northeast-2 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Build, tag, and push image to ECR + env: + ECR_REGISTRY: ${{ secrets.AWS_ECR_REGISTRY }} + ECR_REPOSITORY: streamlit-app + IMAGE_TAG: streamlit-latest + run: | + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . + docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..71a8081 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# Base image +FROM python:3.13.3 + +# Set the working directory +WORKDIR /app + +# Copy requirements file +COPY requirements.txt . + +# Install dependencies +RUN pip install wheel setuptools +RUN python -m pip install --upgrade pip +RUN pip install --no-cache-dir -r requirements.txt +# Copy the application code +COPY . . + +# Expose the port Streamlit runs on +EXPOSE 8501 + +# Command to run the Streamlit app +CMD ["streamlit", "run", "streamlit_app/app.py", "--server.port=8501", "--server.address=0.0.0.0"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5481d15..3a4eee6 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/service/simulation/CurrentSimulator.py b/service/simulation/CurrentSimulator.py index 7e8027b..ff63f5d 100644 --- a/service/simulation/CurrentSimulator.py +++ b/service/simulation/CurrentSimulator.py @@ -1,6 +1,56 @@ -from .SimulatorInterface import SimulatorInterface +from .SimulatorInterface2 import SimulatorInterface2 from simulate_type.simulate_list import generate_current_data +from scipy.stats import truncnorm -class CurrentSimulator(SimulatorInterface): - def generate_data(self, idx: int) -> dict: - return generate_current_data(idx) \ No newline at end of file +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__( + idx=idx, + zone_id=zone_id, + equip_id=equip_id, + interval=interval, + msg_count=msg_count, + conn=conn + ) + # 시뮬레이터 마다 개별적으로 사용하는 속성(토픽, 수집 데이터 초기값) + self.sensor_id = f"UA10C-CUR-2406089{idx}" # 센서 ID + 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.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 + + + # 데이터 생성 로직 정의 + def _generate_data(self) -> dict: + return { + "zoneId": self.zone_id, + "equipId": self.equip_id, + "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 + } + + ################################################ + # 제어 로직을 정의 ( shadow의 desired 상태를 구독하여 제어하는 로직을 구현할 예정) + # sprint 2 에서 더 구체화 예정 + ################################################ + def _apply_desired_state(self, desired_state): + """ + Shadow의 desired 상태를 받아서 센서에 적용 + 예) {"target_Vibration": 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}") + else: + print(f"No target current provided for {self.sensor_id}.") \ No newline at end of file diff --git a/service/simulation/DustSimulator.py b/service/simulation/DustSimulator.py new file mode 100644 index 0000000..18f9fb8 --- /dev/null +++ b/service/simulation/DustSimulator.py @@ -0,0 +1,56 @@ +from .SimulatorInterface2 import SimulatorInterface2 +import random +from scipy.stats import truncnorm + +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__( + idx=idx, + zone_id=zone_id, + equip_id=equip_id, + interval=interval, + msg_count=msg_count, + conn=conn + ) + # 시뮬레이터 마다 개별적으로 사용하는 속성(토픽, 수집 데이터 초기값) + self.sensor_id = f"UA10D-DST-2406089{idx}" # 센서 ID + 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.target_current = None # 초기값 설정(shadow 용) + + 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 + + # 데이터 생성 로직 정의 + def _generate_data(self) -> dict: + return { + "zoneId": self.zone_id, + "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) + } + + ################################################ + # 제어 로직을 정의 ( shadow의 desired 상태를 구독하여 제어하는 로직을 구현할 예정) + # sprint 2 에서 더 구체화 예정 + ################################################ + def _apply_desired_state(self, desired_state): + """ + Shadow의 desired 상태를 받아서 센서에 적용 + 예) {"target_Vibration": 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}") + else: + print(f"No target current provided for {self.sensor_id}.") \ No newline at end of file diff --git a/service/simulation/ExampleSimulator.py b/service/simulation/ExampleSimulator.py index b8fe08c..ab43f7c 100644 --- a/service/simulation/ExampleSimulator.py +++ b/service/simulation/ExampleSimulator.py @@ -1,16 +1,16 @@ -from .SimulationInterface2 import SimulatorInterface2 +from .SimulatorInterface2 import SimulatorInterface2 from simulate_type.simulate_list import generate_temp_data import random class ExampleSimulator(SimulatorInterface2): - def __init__(self, idx: int, space_id:str, manufacture_id:str, interval:int = 5, msg_count:int = 10, conn=None): + def __init__(self, idx: int, zone_id:str, equip_id:str, interval:int = 5, msg_count:int = 10, conn=None): ######################################### # 시뮬레이터에서 공통적으로 사용하는 속성 ######################################### super().__init__( idx=idx, - space_id=space_id, - manufacture_id=manufacture_id, + zone_id=zone_id, + equip_id=equip_id, interval=interval, msg_count=msg_count, conn=conn @@ -29,7 +29,7 @@ def __init__(self, idx: int, space_id:str, manufacture_id:str, interval:int = 5, self.shadow_desired_topic_name = f"$aws/things/KWYTEST/shadow/name/{self.sensor_id}/update/desired" # 센서 데이터 publish용 토픽 - self.topic_name = f"{space_id}/{manufacture_id}/{self.sensor_id}/{self.type}" + self.topic_name = f"sensor/{zone_id}/{equip_id}/{self.sensor_id}/{self.type}" self.target_temperature = None # 초기값 설정(shadow 용) @@ -38,16 +38,18 @@ def __init__(self, idx: int, space_id:str, manufacture_id:str, interval:int = 5, # 예) 온도, 습도, 진동, 전류 등등 ################################################ def _generate_data(self) -> dict: - """ 데이터 생성 메서드 """ return { - "id": self.sensor_id, - "type": self.type, - "temperature": round(random.uniform(20.0 + self.idx, 30.0 + self.idx), 2) + "zoneId": self.zone_id, + "equipId": self.equip_id, + "sensorId": self.sensor_id, + "sensorType": self.type, + "val": round(random.uniform(20.0 + self.idx, 30.0 + self.idx), 2) } ################################################ # 제어 로직을 정의 ( shadow의 desired 상태를 구독하여 제어하는 로직을 구현할 예정) + # sprint 2 에서 더 구체화 예정 ################################################ def _apply_desired_state(self, desired_state): """ diff --git a/service/simulation/HumiditySimulator.py b/service/simulation/HumiditySimulator.py index 8e23260..49ad278 100644 --- a/service/simulation/HumiditySimulator.py +++ b/service/simulation/HumiditySimulator.py @@ -1,6 +1,60 @@ -from .SimulatorInterface import SimulatorInterface -from simulate_type.simulate_list import generate_humidity_data +from .SimulatorInterface2 import SimulatorInterface2 +import random -class HumiditySimulator(SimulatorInterface): - def generate_data(self, idx: int) -> dict: - return generate_humidity_data(idx) \ No newline at end of file +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}.") + + \ No newline at end of file diff --git a/service/simulation/HumidityTempSimulator.py b/service/simulation/HumidityTempSimulator.py index e1fb2a4..a8682fb 100644 --- a/service/simulation/HumidityTempSimulator.py +++ b/service/simulation/HumidityTempSimulator.py @@ -1,9 +1,59 @@ -from .SimulatorInterface import SimulatorInterface -from simulate_type.simulate_list import generate_humidity_temp_data +from .SimulatorInterface2 import SimulatorInterface2 +import random -class HumidityTempSimulator(SimulatorInterface): - def generate_data(self, idx: int) -> dict: +class humidTempSimulator(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-CHS-2406089{idx}" # 센서 ID + self.type = "temp_humid" # 센서 타입 + # shadow 등록용 토픽 + self.shadow_regist_topic_name = f"$aws/things/KWYTEST/shadow/name/{self.sensor_id}/update" + # shadow 제어 명령 구독용 토픽 + self.shadow_desired_topic_name = f"$aws/things/KWYTEST/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 용) + + ################################################ + # 데이터 생성 로직을 정의 (시뮬레이터 마다 다르게 구현) + # 예) 온도, 습도, 진동, 전류 등등 + ################################################ + def _generate_data(self) -> dict: + """ 데이터 생성 메서드 """ + return { + "id": self.sensor_id, + "type": self.type, + "temperature": round(random.uniform(20.0 + self.idx, 30.0 + self.idx), 2) + } + + + ################################################ + # 제어 로직을 정의 ( shadow의 desired 상태를 구독하여 제어하는 로직을 구현할 예정) + ################################################ + def _apply_desired_state(self, desired_state): + """ + Shadow의 desired 상태를 받아서 센서에 적용 + 예) {"target_temperature": 25.0} 이런 명령을 받아 적용 """ - Generate a single data entry containing both humidity and temperature. - """ - return generate_humidity_temp_data(idx) \ No newline at end of file + target_temperature = desired_state.get("target_temperature") + if target_temperature is not None: + self.target_temperature = target_temperature + print(f"Desired state applied: {self.sensor_id} - Target Temperature: {self.target_temperature}") + else: + print(f"No target temperature provided for {self.sensor_id}.") + + \ No newline at end of file diff --git a/service/simulation/SimulatorInterface.py b/service/simulation/SimulatorInterface.py deleted file mode 100644 index 9639cbc..0000000 --- a/service/simulation/SimulatorInterface.py +++ /dev/null @@ -1,6 +0,0 @@ -from abc import ABC, abstractmethod - -class SimulatorInterface(ABC): - @abstractmethod - def generate_data(self, idx: int) -> dict: - pass \ No newline at end of file diff --git a/service/simulation/SimulatorInterface2.py b/service/simulation/SimulatorInterface2.py index aaa8d3c..9ad7929 100644 --- a/service/simulation/SimulatorInterface2.py +++ b/service/simulation/SimulatorInterface2.py @@ -4,12 +4,13 @@ import threading from mqtt_util.publish import AwsMQTT import time +from scipy.stats import truncnorm class SimulatorInterface2(ABC): - def __init__(self, idx: int, space_id: str, manufacture_id: str, interval: int, msg_count: int, conn:AwsMQTT=None): # 센서 idx를 받기 + def __init__(self, idx: int, zone_id: str, equip_id: str, interval: int, msg_count: int, conn:AwsMQTT=None): # 센서 idx를 받기 self.idx = idx # 센서 번호 - self.space_id = space_id # 공간 ID - self.manufacture_id = manufacture_id # 설비 ID + self.zone_id = zone_id # 공간 ID + self.equip_id = equip_id # 설비 ID self.interval = interval # publish 주기 self.msg_count = msg_count # publish 횟수 self.conn = conn # 시뮬레이터 별로 생성된 MQTT 연결 객체를 singleton으로 사용하기 위함 @@ -73,8 +74,8 @@ def _update_shadow(self, status: str = "ON"): "state": { "reported": { "sensorId": self.sensor_id, - "spaceId": self.space_id, - "manufactureId": self.manufacture_id, + "zoneId": self.zone_id, + "equipId": self.equip_id, "type": self.type, "status": status, } @@ -120,4 +121,16 @@ def start_publishing(self): # self.thread.join() def stop(self): - self.stop_event.set() # 스레드 종료 이벤트 설정 \ No newline at end of file + self.stop_event.set() # 스레드 종료 이벤트 설정 + + def generate_truncated_normal(self, mu: float, sigma: float, lower: float = None, upper: float = None) -> float: + # 기본값 설정: 평균 이상의 값만 생성 + if lower is None: + lower = mu + if upper is None: + upper = mu + 3 * sigma # 거의 대부분의 값 포함 (필요시 조정) + + # truncnorm은 정규화된 구간 [a, b]를 사용하므로 변환 필요 + a, b = (lower - mu) / sigma, (upper - mu) / sigma + value = truncnorm.rvs(a, b, loc=mu, scale=sigma) + return round(value, 2) diff --git a/service/simulation/TempSimulator.py b/service/simulation/TempSimulator.py index a33ebe6..e204eb0 100644 --- a/service/simulation/TempSimulator.py +++ b/service/simulation/TempSimulator.py @@ -1,6 +1,77 @@ -from .SimulatorInterface import SimulatorInterface -from simulate_type.simulate_list import generate_temp_data +from .SimulatorInterface2 import SimulatorInterface2 +import random +from scipy.stats import truncnorm -class TempSimulator(SimulatorInterface): - def generate_data(self, idx: int) -> dict: - return generate_temp_data(idx) \ No newline at end of file + +class TempSimulator(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"UA10T-TEM-2406089{idx}" # 센서 ID + self.type = "temp" # 센서 타입 + # 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 용) + + self.mu = 25 # 평균 온도 (정상 범위: 18~21℃) + self.sigma = 10 # 표준편차 (온도의 변동폭) + + # 절단 범위 설정 (최소값 -35℃, 최대값 50℃로 설정) + self.lower = -35 + self.upper = 50 + + # 정규분포 범위의 a, b 값 계산 + self.a = (self.lower - self.mu) / self.sigma + self.b = (self.upper - self.mu) / self.sigma + + ################################################z + # 데이터 생성 로직을 정의 (시뮬레이터 마다 다르게 구현) + # 예) 온도, 습도, 진동, 전류 등등 + ################################################ + def _generate_data(self) -> dict: + """ 데이터 생성 메서드 """ + return { + "zoneId": self.zone_id, + "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) + } + + ################################################ + # 제어 로직을 정의 ( shadow의 desired 상태를 구독하여 제어하는 로직을 구현할 예정) + ################################################ + def _apply_desired_state(self, desired_state): + """ + Shadow의 desired 상태를 받아서 센서에 적용 + 예) {"target_humid": 25.0} 이런 명령을 받아 적용 + """ + target_humid = desired_state.get("target_humid") + if target_humid is not None: + self.target_humid = target_humid + print(f"Desired state applied: {self.sensor_id} - Target humid: {self.target_humid}") + else: + print(f"No target humid provided for {self.sensor_id}.") + + \ No newline at end of file diff --git a/service/simulation/VibrationSimulator.py b/service/simulation/VibrationSimulator.py index 608cdea..06ff015 100644 --- a/service/simulation/VibrationSimulator.py +++ b/service/simulation/VibrationSimulator.py @@ -1,6 +1,61 @@ -from .SimulatorInterface import SimulatorInterface +from .SimulatorInterface2 import SimulatorInterface2 from simulate_type.simulate_list import generate_vibration_data +import random +from scipy.stats import truncnorm -class VibrationSimulator(SimulatorInterface): - def generate_data(self, idx: int) -> dict: - return generate_vibration_data(idx) \ No newline at end of file +class VibrationSimulator(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"UA10V-VIB-2406089{idx}" # Sensor ID + self.type = "vibration" # Sensor type + 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.target_vibration = None # Initial value for shadow) + self.mu = 3.5 # 평균 진동값 + self.sigma = 2 # 표준편차 (진동의 변동폭) + + # 절단 범위 설정 (최소값 0, 최대값 10으로 설정) + self.lower = 0 + self.upper = 10 + + # 정규분포 범위의 a, b 값 계산 + self.a = (self.lower - self.mu) / self.sigma + self.b = (self.upper - self.mu) / self.sigma + + # 데이터 생성 로직을 정의 (시뮬레이터 마다 다르게 구현) + def _generate_data(self) -> dict: + return { + "zoneId": self.zone_id, + "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) + } + + + ################################################ + # 제어 로직을 정의 ( shadow의 desired 상태를 구독하여 제어하는 로직을 구현할 예정) + # sprint 2 에서 더 구체화 예정 + ################################################ + def _apply_desired_state(self, desired_state): + """ + Shadow의 desired 상태를 받아서 센서에 적용 + 예) {"target_Vibration": 25.0} 이런 명령을 받아 적용 + """ + target_vibration = desired_state.get("target_vibration") + if target_vibration is not None: + self.target_vibration = target_vibration + print(f"Desired state applied: {self.sensor_id} - Target Vibration: {self.target_vibration}") + else: + print(f"No target vibration provided for {self.sensor_id}.") + \ No newline at end of file diff --git a/service/simulation/VocSimulator.py b/service/simulation/VocSimulator.py new file mode 100644 index 0000000..ebab0c3 --- /dev/null +++ b/service/simulation/VocSimulator.py @@ -0,0 +1,47 @@ +from .SimulatorInterface2 import SimulatorInterface2 +import random + +class VocSimulator(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"UA10V-VOC-2406089{idx}" # 센서 ID + self.type = "voc" # 센서 타입 + 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.target_current = None # 초기값 설정(shadow 용) + + # 데이터 생성 로직 정의 + def _generate_data(self) -> dict: + return { + "zoneId": self.zone_id, + "equipId": self.equip_id, + "sensorId": self.sensor_id, + "sensorType": self.type, + "val": round(random.uniform(5.0 + self.idx, 50.0 + self.idx), 2) + } + + ################################################ + # 제어 로직을 정의 ( shadow의 desired 상태를 구독하여 제어하는 로직을 구현할 예정) + # sprint 2 에서 더 구체화 예정 + ################################################ + def _apply_desired_state(self, desired_state): + """ + Shadow의 desired 상태를 받아서 센서에 적용 + 예) {"target_Vibration": 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}") + else: + print(f"No target current provided for {self.sensor_id}.") \ No newline at end of file diff --git a/service/simulation/factory.py b/service/simulation/factory.py index 2237459..0173a60 100644 --- a/service/simulation/factory.py +++ b/service/simulation/factory.py @@ -1,29 +1,34 @@ -from service.simulation import TempSimulator +from service.simulation.TempSimulator import TempSimulator from service.simulation.HumiditySimulator import HumiditySimulator -from service.simulation.HumidityTempSimulator import HumidityTempSimulator from service.simulation.VibrationSimulator import VibrationSimulator from service.simulation.CurrentSimulator import CurrentSimulator +from service.simulation.DustSimulator import DustSimulator from service.simulation.ExampleSimulator import ExampleSimulator +from service.simulation.VocSimulator import VocSimulator from mqtt_util.publish import AwsMQTT from typing import List # from .SimulatorInterface import SimulatorInterface -from .SimulationInterface2 import SimulatorInterface2 -def get_simulator(simulator_type: str, idx: int, space_id: str, manufacture_id: str, interval: int = 5, msg_count: int = 10, conn:AwsMQTT=None)-> List[SimulatorInterface2]: +from .SimulatorInterface2 import SimulatorInterface2 +def get_simulator(simulator_type: str, idx: int, zone_id: str, equip_id: str, interval: int = 5, msg_count: int = 10, conn:AwsMQTT=None)-> List[SimulatorInterface2]: simulator_entity_list = [] for i in range(idx): if simulator_type == "temp": - simulator_entity_list.append(TempSimulator(i, space_id, manufacture_id, interval, msg_count, conn)) + simulator_entity_list.append(TempSimulator(i, zone_id, equip_id, interval, msg_count, conn)) elif simulator_type == "humidity": - simulator_entity_list.append(HumiditySimulator()) - elif simulator_type == "humidity_temp": - simulator_entity_list.append(HumidityTempSimulator()) + simulator_entity_list.append(HumiditySimulator(i, zone_id, equip_id, interval, msg_count, conn)) + # elif simulator_type == "humidity_temp": + # simulator_entity_list.append(HumidityTempSimulator(i, zone_id, equip_id, interval, msg_count, conn)) elif simulator_type == "vibration": - simulator_entity_list.append(VibrationSimulator()) + simulator_entity_list.append(VibrationSimulator(i, zone_id, equip_id, interval, msg_count, conn)) elif simulator_type == "current": - simulator_entity_list.append(CurrentSimulator()) + simulator_entity_list.append(CurrentSimulator(i, zone_id, equip_id, interval, msg_count, conn)) + elif simulator_type == "dust": + simulator_entity_list.append(DustSimulator(i, zone_id, equip_id, interval, msg_count, conn)) elif simulator_type == "example": - simulator_entity_list.append(ExampleSimulator(i, space_id, manufacture_id, interval, msg_count, conn)) + simulator_entity_list.append(ExampleSimulator(i, zone_id, equip_id, interval, msg_count, conn)) + elif simulator_type == "voc": + simulator_entity_list.append(VocSimulator(i, zone_id, equip_id, interval, msg_count, conn)) else: raise ValueError(f"Unknown simulator type: {simulator_type}") return simulator_entity_list \ No newline at end of file diff --git a/service/simulation/simulateTest.py b/service/simulation/simulateTest.py index 6dc949a..5bd76f9 100644 --- a/service/simulation/simulateTest.py +++ b/service/simulation/simulateTest.py @@ -3,85 +3,83 @@ import sys import argparse import json +import threading from awscrt import mqtt from mqtt_util.publish import AwsMQTT # 각 시뮬레이션 인터페이스에서 해당 데이터들을 사용, 메인에선 사용 X # from simulate_type.simulate_list import generate_temp_data, generate_humidity_data, generate_humidity_temp_data, generate_wearable_data, generate_vibration_data, generate_current_data from .factory import get_simulator -# 시뮬레이션 함수 -def simulate_data(count, interval, manufacture_id, space_id, sensor_num=2, conn: AwsMQTT = None, simulator_type="temp"): - try: - print(f"Simulating {simulator_type} data stream for {count} entries with {interval} second intervals... (Press Ctrl+C to stop)") +conn = AwsMQTT() + +# 스레드에서 실행될 시뮬레이션 함수 +def run_simulator(simulator, count, interval): + for _ in range(count): + data = simulator.start_publishing() + print(json.dumps(data, indent=4)) # 데이터를 JSON 형식으로 출력 + time.sleep(interval) - # 시뮬레이터 생성 - simulators = get_simulator( - idx=sensor_num, - interval=interval, - msg_count=count, - manufacture_id=manufacture_id, - space_id=space_id, - simulator_type=simulator_type, - conn=conn - ) +def run_simulator_from_streamlit(simulator_type, count, interval, sensor_num, space_id, manufacture_id): + simulators = get_simulator( + conn=AwsMQTT(), + simulator_type=simulator_type, + idx=sensor_num, + space_id=space_id, + manufacture_id=manufacture_id, + interval=interval, + msg_count=count + ) - for simulator in simulators: - simulator.start_publishing() + for simulator in simulators: + for _ in range(count): + data = simulator.start_publishing() + print(json.dumps(data, indent=4)) # 데이터를 JSON 형식으로 출력 + time.sleep(interval) +# 시뮬레이션 함수 +def run_simulation_from_json(json_file_path): + # JSON 파일 읽기 + with open(json_file_path, 'r', encoding="utf-8") as file: + config = json.load(file) - # 스레드 상태를 모니터링 - while any(sim.thread.is_alive() for sim in simulators): - time.sleep(0.1) + devices = config.get("devices", []) + if not devices: + print("No devices found in the configuration.") + return - except KeyboardInterrupt: - print("\nSimulation stopped by user.") - for simulator in simulators: - simulator.stop() - finally: - conn.disconnect() + threads = [] # 스레드를 저장할 리스트 -# 콜백 함수: MQTT로 데이터를 전송 -def mqtt_publish_callback(data, topic): - # JSON 직렬화 - payload = json.dumps(data) - # MQTT 연결 객체를 통해 publish 호출 - - conn.publish( - topic=topic, - payload=payload, - qos=mqtt.QoS.AT_LEAST_ONCE - ) - print(f"Published: {payload}") + for device in devices: + count = device.get("count", 10) + interval = device.get("interval", 1.0) + equip_id = device.get("equip_id", "UNKNOWN") + zone_id = device.get("zone_id", "UNKNOWN") + simulator_type = device.get("simulator", "temp") + sensor_num = device.get("sensor_num", 1) -# 메인 함수 -def main(): - global conn - # CLI 매개변수 파싱 # - parser = argparse.ArgumentParser(description="Simulate various data types and publish them via MQTT.") - - parser.add_argument("--count", type=int, default=10, help="Number of data entries to generate.") - parser.add_argument("--interval", type=float, default=5.0, help="Interval between data entries in seconds.") - parser.add_argument("--manufacture_id", type=str, default="SBID-001", help="Manufacture ID.") - parser.add_argument("--space_id", type=str, default="PID-001", help="Space ID.") - parser.add_argument("--simulator", type=str, choices=["temp", "humidity","humidity_temp", "vibration", "current" ], default="example", help="Type of data simulator.") - parser.add_argument("--sensor_num", type=int, default=2, help="Number of sensors to simulate.") - args = parser.parse_args() + print(f"Starting simulation for {simulator_type} with {sensor_num} sensors...") - # IoT Core MQTT 연결 객체 - conn = AwsMQTT() + # 시뮬레이터 생성 + simulators = get_simulator( + conn = conn, + simulator_type=simulator_type, + idx=sensor_num, + zone_id=zone_id, + equip_id=equip_id, + interval=interval, + msg_count=count + ) - # Shadow에 디바이스 등록 - - # 시뮬레이션 실행, 콜백 함수 전달 - simulate_data( - count=args.count, - interval=args.interval, - space_id=args.space_id, - manufacture_id=args.manufacture_id, - sensor_num=args.sensor_num, - conn=conn, - simulator_type=args.simulator - ) + # 데이터 생성 및 출력 + for simulator in simulators: + # for _ in range(count): + # data = simulator.start_publishing() + # # print(json.dumps(data, indent=4)) # 데이터를 JSON 형식으로 출력 + # time.sleep(interval) + thread = threading.Thread(target=run_simulator, args=(simulator, count, interval)) + threads.append(thread) + thread.start() -# 테스트용 메인 함수 (index.py에도 존재함) if __name__ == "__main__": - main() \ No newline at end of file + # JSON 파일 경로 + json_file_path = "simulation_cconfig.json" + run_simulation_from_json(json_file_path) \ No newline at end of file diff --git a/simulate_type/simulate_list.py b/simulate_type/simulate_list.py index cf34b3d..0ffded1 100644 --- a/simulate_type/simulate_list.py +++ b/simulate_type/simulate_list.py @@ -10,20 +10,20 @@ def generate_temp_data(sensor_idx): } # 습도 센서 -def generate_humidity_data(sensor_idx): +def generate_humid_data(sensor_idx): return { "id": f"UA10H-CHS-2406089{sensor_idx}", "type": "SUP", - "humidity": round(random.uniform(20.0 + sensor_idx, 80.0 + sensor_idx), 2) + "humid": round(random.uniform(20.0 + sensor_idx, 80.0 + sensor_idx), 2) } # 온습도 센서 -def generate_humidity_temp_data(sensor_idx): +def generate_humid_temp_data(sensor_idx): return { "id": f"UA10H-CHS-2406089{sensor_idx}", "type": "ON-SUP", "temperature": round(random.uniform(20.0 + sensor_idx, 30.0 + sensor_idx), 2), - "humidity": round(random.uniform(20.0 + sensor_idx, 80.0 + sensor_idx), 2) + "humid": round(random.uniform(20.0 + sensor_idx, 80.0 + sensor_idx), 2) } diff --git a/simulation_cconfig.json b/simulation_cconfig.json new file mode 100644 index 0000000..1890c2c --- /dev/null +++ b/simulation_cconfig.json @@ -0,0 +1,28 @@ +{ + "devices": [ + { + "count": 1, + "interval": 10.0, + "equip_id": "SBID-124", + "zone_id": "PID-711", + "simulator": "temp", + "sensor_num": 2 + }, + { + "count": 1, + "interval": 10.0, + "equip_id": "SBID-125", + "zone_id": "PID-790", + "simulator": "humidity", + "sensor_num": 3 + }, + { + "count": 1, + "interval": 10.0, + "equip_id": "SBID-126", + "zone_id": "PID-791", + "simulator": "vibration", + "sensor_num": 4 + } + ] +} \ No newline at end of file diff --git a/simulation_config.db b/simulation_config.db new file mode 100644 index 0000000..d419dc7 Binary files /dev/null and b/simulation_config.db differ diff --git a/streamlit_app/app.py b/streamlit_app/app.py new file mode 100644 index 0000000..accf9c8 --- /dev/null +++ b/streamlit_app/app.py @@ -0,0 +1,178 @@ +import sys +import os +import streamlit as st +import json +import sqlite3 +import threading +import time + +# 프로젝트 루트 디렉터리를 sys.path에 추가 +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from service.simulation.simulateTest import run_simulator_from_streamlit + +# Filepath for the JSON configuration +JSON_FILE_PATH = "./simulation_cconfig.json" +DB_FILE_PATH = "./simulation_config.db" + +# Thread management +simulation_threads = {} +stop_events = {} + +# Load JSON data +def load_json(): + if os.path.exists(JSON_FILE_PATH): + with open(JSON_FILE_PATH, "r") as file: + try: + return json.load(file) + except json.JSONDecodeError as e: + st.error(f"Error loading JSON: {e}") + return {"devices": []} + return {"devices": []} + +# Save JSON data +def save_json(data): + with open(JSON_FILE_PATH, "w") as file: + json.dump(data, file, indent=4) + +# Initialize SQLite database +def init_db(): + conn = sqlite3.connect(DB_FILE_PATH) + cursor = conn.cursor() + cursor.execute(""" + CREATE TABLE IF NOT EXISTS devices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + count INTEGER, + interval REAL, + manufacture_id TEXT, + space_id TEXT, + simulator TEXT, + sensor_num INTEGER + ) + """) + conn.commit() + conn.close() + +# Save data to SQLite +def save_to_db(data): + conn = sqlite3.connect(DB_FILE_PATH) + cursor = conn.cursor() + cursor.execute("DELETE FROM devices") # Clear existing data + for device in data["devices"]: + cursor.execute(""" + INSERT INTO devices (count, interval, manufacture_id, space_id, simulator, sensor_num) + VALUES (?, ?, ?, ?, ?, ?) + """, (device["count"], device["interval"], device["manufacture_id"], device["space_id"], device["simulator"], device["sensor_num"])) + conn.commit() + conn.close() + +# Load data from SQLite +def load_from_db(): + conn = sqlite3.connect(DB_FILE_PATH) + cursor = conn.cursor() + cursor.execute("SELECT count, interval, manufacture_id, space_id, simulator, sensor_num FROM devices") + rows = cursor.fetchall() + conn.close() + return {"devices": [dict(zip(["count", "interval", "manufacture_id", "space_id", "simulator", "sensor_num"], row)) for row in rows]} + +# Function to run simulation with stop functionality +def run_simulation_with_stop(simulator_type, count, interval, sensor_num, space_id, manufacture_id, stop_event): + for _ in range(count): + if stop_event.is_set(): # Stop 이벤트가 설정되었는지 확인 + print(f"Stopping simulation for {simulator_type}") + break + run_simulator_from_streamlit(simulator_type, count, interval, sensor_num, space_id, manufacture_id) + time.sleep(interval) # 시뮬레이션 간격 + +# Streamlit app +def main(): + st.title("Simulation Configuration Manager") + + # Initialize session state for data + if "data" not in st.session_state: + st.session_state.data = load_json() + + # Sidebar options to load data + if st.sidebar.button("Load from JSON"): + st.session_state.data = load_json() + st.success("Loaded data from JSON.") + st.rerun() + elif st.sidebar.button("Load from SQLite"): + st.session_state.data = load_from_db() + st.success("Loaded data from SQLite.") + st.rerun() + + # Display devices in blocks + st.header("Devices") + if "devices" in st.session_state.data and st.session_state.data["devices"]: + for i, device in enumerate(st.session_state.data["devices"]): + with st.expander(f"Device {i + 1}"): + st.subheader(f"Device {i + 1} Details") + device["count"] = st.number_input(f"Count (Device {i + 1})", value=device["count"], key=f"count_{i}") + device["interval"] = st.number_input(f"Interval (Device {i + 1})", value=device["interval"], key=f"interval_{i}") + device["manufacture_id"] = st.text_input(f"Manufacture ID (Device {i + 1})", value=device["manufacture_id"], key=f"manufacture_id_{i}") + device["space_id"] = st.text_input(f"Space ID (Device {i + 1})", value=device["space_id"], key=f"space_id_{i}") + device["simulator"] = st.text_input(f"Simulator (Device {i + 1})", value=device["simulator"], key=f"simulator_{i}") + device["sensor_num"] = st.number_input(f"Sensor Num (Device {i + 1})", value=device["sensor_num"], key=f"sensor_num_{i}") + + # Run Simulation Button + if st.button(f"Run Simulation for Device {i + 1}", key=f"run_{i}"): + if i not in simulation_threads or not simulation_threads[i].is_alive(): + stop_events[i] = threading.Event() + st.write(f"Starting simulation for Device {i + 1} with stop_event: {stop_events[i]}") # 디버깅 출력 + thread = threading.Thread(target=run_simulation_with_stop, args=( + device["simulator"], + device["count"], + device["interval"], + device["sensor_num"], + device["space_id"], + device["manufacture_id"], + stop_events[i] + )) + simulation_threads[i] = thread + thread.start() + st.success(f"Simulation for Device {i + 1} started.") + else: + st.warning(f"Simulation for Device {i + 1} is already running.") + + # Stop Simulation Button + if st.button(f"Stop Simulation for Device {i + 1}", key=f"stop_{i}"): + if i in stop_events: + stop_events[i].set() # Stop 이벤트 설정 + st.write(f"Stopping simulation for Device {i + 1} with stop_event: {stop_events[i]}") # 디버깅 출력 + st.success(f"Simulation for Device {i + 1} stopped.") + else: + st.warning(f"No simulation is running for Device {i + 1}.") + + # Delete Device Button + if st.button(f"Delete Device {i + 1}", key=f"delete_{i}"): + st.session_state.data["devices"].pop(i) + st.rerun() + else: + st.write("No devices found. Please load data or add a new device.") + + # Add new device + st.header("Add New Device") + if st.button("Add Device"): + st.session_state.data["devices"].append({ + "count": 1, + "interval": 1.0, + "manufacture_id": "NEW_ID", + "space_id": "NEW_SPACE", + "simulator": "temp", + "sensor_num": 1 + }) + st.rerun() + + # Save options + st.sidebar.header("Save Options") + if st.sidebar.button("Save to JSON"): + save_json(st.session_state.data) + st.success("Saved data to JSON.") + if st.sidebar.button("Save to SQLite"): + save_to_db(st.session_state.data) + st.success("Saved data to SQLite.") + +if __name__ == "__main__": + init_db() + main() \ No newline at end of file