diff --git a/.github/workflows/docker-build-develop.yml b/.github/workflows/docker-build-develop.yml index e155661..bd4d072 100644 --- a/.github/workflows/docker-build-develop.yml +++ b/.github/workflows/docker-build-develop.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - main + - develop jobs: test-and-deploy: @@ -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 \ No newline at end of file diff --git a/service/simulation/SimulatorInterface2.py b/service/simulation/SimulatorInterface2.py index 4f5f375..244b288 100644 --- a/service/simulation/SimulatorInterface2.py +++ b/service/simulation/SimulatorInterface2.py @@ -2,12 +2,13 @@ from awscrt import mqtt import json import threading +from threading import Event from mqtt_util.publish import AwsMQTT import time from scipy.stats import truncnorm class SimulatorInterface2(ABC): - def __init__(self, idx: int, zone_id: str, equip_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, stop_event: Event = None): # 센서 idx를 받기 self.idx = idx # 센서 번호 self.zone_id = zone_id # 공간 ID self.equip_id = equip_id # 설비 ID @@ -15,7 +16,8 @@ def __init__(self, idx: int, zone_id: str, equip_id: str, interval: int, msg_cou self.msg_count = msg_count # publish 횟수 self.conn = conn # 시뮬레이터 별로 생성된 MQTT 연결 객체를 singleton으로 사용하기 위함 self.thead = None # 스레드 객체 - self.stop_event = threading.Event() # 스레드 종료 이벤트 객체 + # stop_event가 None이면 새 Event 객체 생성 + self.stop_event = stop_event if stop_event is not None else Event() ########################################################## # @abstractmethod 시뮬레이터 마다 로직이 달라 구현해야되는 메서드 ########################################################## @@ -160,6 +162,37 @@ def publish_value(self, sensor_type: str, value: float): self.conn.publish(topic, payload, mqtt.QoS.AT_LEAST_ONCE) print(f"Published data to {topic}: {payload}, {threading.current_thread().name}") def stop_publishing(self): - """시뮬레이터 중지""" - self.stop_event.set() - print(f"[{self.__class__.__name__}] Stopping publishing...") \ No newline at end of file + # """시뮬레이터 중지""" + # self.stop_event.set() + # print(f"[{self.__class__.__name__}] Stopping publishing...") + """시뮬레이터 중지 및 리소스 정리""" + if hasattr(self, 'stop_event') and self.stop_event: + print(f"[{self.__class__.__name__}] Setting stop event...") + self.stop_event.set() # 중지 이벤트 설정 + + # 스레드가 있고 살아있다면 종료 대기 (최대 3초) + if hasattr(self, 'thread') and self.thread and self.thread.is_alive(): + print(f"[{self.__class__.__name__}] Waiting for thread to terminate...") + self.thread.join(timeout=3) + + # 여전히 살아있다면 경고 + if self.thread.is_alive(): + print(f"Warning: Thread for {self.__class__.__name__} could not be terminated normally") + + # shadow 상태 업데이트 - 센서 OFF 상태 알림 + try: + self._update_shadow(status="OFF") + print(f"[{self.__class__.__name__}] Shadow updated to OFF state") + except Exception as e: + print(f"[{self.__class__.__name__}] Error updating shadow: {e}") + + # 추가 리소스 정리 + self._cleanup_resources() + + print(f"[{self.__class__.__name__}] Successfully stopped") + else: + print(f"[{self.__class__.__name__}] No stop_event available") + + def _cleanup_resources(self): + """자식 클래스에서 오버라이드하여 추가 리소스 정리 가능""" + pass \ No newline at end of file diff --git a/simulation_cconfig.json b/simulation_cconfig.json index 82a397a..bb9dc3a 100644 --- a/simulation_cconfig.json +++ b/simulation_cconfig.json @@ -2,7 +2,7 @@ "devices": [ { "count": 5, - "interval": 5, + "interval": 3, "equip_id": "20250507171316-389", "zone_id": "20250507165750-827", "simulator": "real_sensor", @@ -10,11 +10,11 @@ }, { "count": 5, - "interval": 5, + "interval": 3, "equip_id": "20250507171316-389", "zone_id": "20250507165750-827", - "simulator": "current", + "simulator": "vibration", "sensor_num": 1 - } + } ] } \ No newline at end of file diff --git a/streamlit_app/app.py b/streamlit_app/app.py index 590e003..4bf7dd2 100644 --- a/streamlit_app/app.py +++ b/streamlit_app/app.py @@ -5,7 +5,7 @@ import sqlite3 import threading import time - +from threading import Event # 프로젝트 루트 디렉터리를 sys.path에 추가 sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) @@ -76,37 +76,26 @@ def load_from_db(): return {"devices": [dict(zip(["count", "interval", "equip_id", "zone_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, zone_id, equip_id, stop_event): - # RealSensor와 가상 시뮬레이터를 구분하여 처리 - if simulator_type == "real_sensor": - # RealSensor 모드: 쓰레드를 실행하고 포트 해제를 위해 join() 사용 - threads = run_simulator_from_streamlit( - simulator_type, count, interval, - sensor_num, zone_id, equip_id, - stop_event # stop_event 전달 중요! - ) - - # join() 호출 제거 - 쓰레드가 백그라운드에서 실행되도록 함 - print(f"RealSensor threads started in background. Count: {len(threads or [])}") +def run_simulation_with_stop(simulator_type, count, interval, sensor_num, zone_id, equip_id, stop_event: 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, zone_id, equip_id) + # time.sleep(interval) # 시뮬레이션 간격 - # 선택사항: 쓰레드 정리를 위한 참조 저장 - # 현재의 코드에서는 이미 simulation_threads에 참조가 저장되므로 추가 작업 필요 없음 - else: - # 가상 시뮬레이터 모드: 기존에 잘 작동하던 방식 사용 - for _ in range(count): - if stop_event.is_set(): # Stop 이벤트가 설정되었는지 확인 - print(f"Stopping simulation for {simulator_type}") - break - - # 시뮬레이터 한 번 실행 - run_simulator_from_streamlit( - simulator_type, 1, interval, # count=1로 한 번만 실행 - sensor_num, zone_id, equip_id, - stop_event - ) - - time.sleep(interval) # 시뮬레이션 간격 - + + # ① 시뮬레이터(혹은 RealSensor) 실행 → 쓰레드 리스트 반환 + threads = run_simulator_from_streamlit( + simulator_type, count, interval, + sensor_num, zone_id, equip_id,stop_event) + + # ② 반환된 쓰레드가 있으면 모두 종료될 때까지 기다렸다가 포트 해제 + for th in threads or []: # None 방어 + if th is not None: + th.join() # ← 여기서 blocking, ser.close() 까지 완료 + + # Streamlit app def main(): st.title("Simulation Configuration Manager") @@ -135,7 +124,19 @@ def main(): device["interval"] = st.number_input(f"Interval (Device {i + 1})", value=device["interval"], key=f"interval_{i}") device["equip_id"] = st.text_input(f"Manufacture ID (Device {i + 1})", value=device["equip_id"], key=f"equip_id_{i}") device["zone_id"] = st.text_input(f"Space ID (Device {i + 1})", value=device["zone_id"], key=f"zone_id_{i}") - device["simulator"] = st.text_input(f"Simulator (Device {i + 1})", value=device["simulator"], key=f"simulator_{i}") + # device["simulator"] = st.text_input(f"Simulator (Device {i + 1})", value=device["simulator"], key=f"simulator_{i}") + #드랍다운 선택 형식으로 시뮬레이터 적용 + simulator_options = ["temp", "humidity", "vibration", "current", "dust", "voc", "real_sensor"] + device["simulator"] = st.selectbox( + f"Simulator (Device {i + 1})", + options=simulator_options, + index=simulator_options.index(device["simulator"]) if device["simulator"] in simulator_options else 0, + key=f"simulator_{i}" + ) + # real_sensor가 선택된 경우 경고 메시지 표시 + if device["simulator"] == "real_sensor": + st.warning("⚠️ 'real_sensor'는 로컬 환경에서만 사용 가능하며, 센서를 USB 포트(COM3)에 연결해야 합니다.") + # 센서 갯수 입력 device["sensor_num"] = st.number_input(f"Sensor Num (Device {i + 1})", value=device["sensor_num"], key=f"sensor_num_{i}") # Run Simulation Button @@ -198,7 +199,6 @@ def main(): 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