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
43 changes: 38 additions & 5 deletions service/simulation/SimulatorInterface2.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@
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
self.interval = interval # publish 주기
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 시뮬레이터 마다 로직이 달라 구현해야되는 메서드
##########################################################
Expand Down Expand Up @@ -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...")
# """시뮬레이터 중지"""
# 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
8 changes: 4 additions & 4 deletions simulation_cconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
"devices": [
{
"count": 5,
"interval": 5,
"interval": 3,
"equip_id": "20250507171316-389",
"zone_id": "20250507165750-827",
"simulator": "real_sensor",
"sensor_num": 1
},
{
"count": 5,
"interval": 5,
"interval": 3,
"equip_id": "20250507171316-389",
"zone_id": "20250507165750-827",
"simulator": "current",
"simulator": "vibration",
"sensor_num": 1
}
}
]
}
66 changes: 33 additions & 33 deletions streamlit_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__), "..")))

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Loading