diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3d9a0a --- /dev/null +++ b/.gitignore @@ -0,0 +1,191 @@ +# Created by https://www.toptal.com/developers/gitignore/api/linux,python +# Edit at https://www.toptal.com/developers/gitignore?templates=linux,python + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/linux,python \ No newline at end of file diff --git a/README.md b/README.md index 3c48d74..b6d04dd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # iot_raspberryPi controls GPIO devices using Python + +## requirements.txt 사용방법 +``` +pip install -r requirements.txt +``` diff --git a/database/db_connect.py b/database/db_connect.py new file mode 100644 index 0000000..1e7814a --- /dev/null +++ b/database/db_connect.py @@ -0,0 +1,223 @@ +import os +import mysql.connector +from mysql.connector import Error +from dotenv import load_dotenv + +# =============================== +# DB 클래스 설정 +# =============================== +class DBManager: + """ + 데이터베이스 연결 및 CRUD 작업을 관리하는 클래스 + """ + def __init__(self): + """ + 클래스 인스턴스 생성 시 .env 파일에서 환경 변수를 로드하고 DB에 연결합니다. + """ + load_dotenv() + db_config = { + "host": os.getenv("DB_HOST"), + "user": os.getenv("DB_USER"), + "password": os.getenv("DB_PASSWORD"), + "database": os.getenv("DB_DATABASE") + } + + self.conn = None + try: + self.conn = mysql.connector.connect(**db_config) + if self.conn.is_connected(): + print("✅ DB 연결 성공") + except Error as e: + print(f"❌ DB 연결 실패: {e}") + + def __del__(self): + """ + 클래스 인스턴스 소멸 시 DB 연결을 자동으로 닫습니다. + """ + if self.conn and self.conn.is_connected(): + self.conn.close() + print("🔗 DB 연결 해제") + + # =============================== + # 범용 CRUD 메서드 + # =============================== + def insert_data(self, table, data): + """ + 지정된 테이블에 데이터를 삽입합니다. + :param table: 테이블 이름 (str) + :param data: 삽입할 데이터 {'컬럼명': 값} (dict) + """ + if not self.conn or not self.conn.is_connected(): + print("❌ DB에 연결되어 있지 않습니다.") + return + + columns = ', '.join(data.keys()) + placeholders = ', '.join(['%s'] * len(data)) + sql = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})" + + cur = self.conn.cursor() + try: + cur.execute(sql, list(data.values())) + self.conn.commit() + print(f"✅ [{table}] 데이터 삽입 성공") + except Error as e: + print(f"❌ [{table}] 데이터 삽입 실패: {e}") + finally: + cur.close() + + def select_data(self, table, columns="*", where=None): + """ + 지정된 테이블에서 데이터를 조회합니다. + :param table: 조회할 테이블 이름 (str) + :param columns: 조회할 컬럼 (str, 기본값: '*') + :param where: WHERE 조건 {'컬럼명': 값} (dict, optional) + :return: 조회 결과 (list of dicts) + """ + if not self.conn or not self.conn.is_connected(): + print("❌ DB에 연결되어 있지 않습니다.") + return [] + + sql = f"SELECT {columns} FROM {table}" + params = [] + if where: + where_clauses = [f"{key} = %s" for key in where.keys()] + sql += " WHERE " + " AND ".join(where_clauses) + params = list(where.values()) + + cur = self.conn.cursor(dictionary=True) # 결과를 딕셔너리로 받기 + results = [] + try: + cur.execute(sql, params) + results = cur.fetchall() + except Error as e: + print(f"❌ [{table}] 데이터 조회 실패: {e}") + finally: + cur.close() + return results + + def update_data(self, table, data, where): + """ + 지정된 테이블의 데이터를 수정합니다. + :param data: 수정할 데이터 {'컬럼명': 새 값} (dict) + :param where: WHERE 조건 {'컬럼명': 값} (dict) + """ + if not self.conn or not self.conn.is_connected(): + print("❌ DB에 연결되어 있지 않습니다.") + return + + set_clauses = ', '.join([f"{key} = %s" for key in data.keys()]) + where_clauses = ' AND '.join([f"{key} = %s" for key in where.keys()]) + sql = f"UPDATE {table} SET {set_clauses} WHERE {where_clauses}" + + params = list(data.values()) + list(where.values()) + + cur = self.conn.cursor() + try: + cur.execute(sql, params) + self.conn.commit() + print(f"🔄 [{table}] 데이터 수정 성공 (조건: {where})") + except Error as e: + print(f"❌ [{table}] 데이터 수정 실패: {e}") + finally: + cur.close() + + def delete_data(self, table, where): + """ + 지정된 테이블의 데이터를 삭제합니다. + :param where: WHERE 조건 {'컬럼명': 값} (dict) + """ + if not self.conn or not self.conn.is_connected(): + print("❌ DB에 연결되어 있지 않습니다.") + return + + where_clauses = ' AND '.join([f"{key} = %s" for key in where.keys()]) + sql = f"DELETE FROM {table} WHERE {where_clauses}" + + cur = self.conn.cursor() + try: + cur.execute(sql, list(where.values())) + self.conn.commit() + print(f"🗑️ [{table}] 데이터 삭제 성공 (조건: {where})") + except Error as e: + print(f"❌ [{table}] 데이터 삭제 실패: {e}") + finally: + cur.close() + +# =============================== +# DB 조작 함수 +# =============================== +def insert_environment(conn, device_id, temp=None, hum=None, gas=None): + sql = """ + INSERT INTO environment_data (device_id, temperature, humidity, gas_level) + VALUES (%s, %s, %s, %s) + """ + cur = conn.cursor() + cur.execute(sql, (device_id, temp, hum, gas)) + conn.commit() + cur.close() + +def insert_event(conn, device_id, office_id, etype, action, value, note): + sql = """ + INSERT INTO event_log (device_id, user_id, office_id, event_type, event_action, value, note) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """ + cur = conn.cursor() + cur.execute(sql, (device_id, user_id, office_id, etype, action, value, note)) + conn.commit() + cur.close() + print(f"🚨 EVENT_LOG → [{etype}] {action} ({note})") + +def update_device_status(conn, device_id, new_status): + sql = "UPDATE devices SET status=%s, last_updated=NOW() WHERE device_id=%s" + cur = conn.cursor() + cur.execute(sql, (new_status, device_id)) + conn.commit() + cur.close() + print(f"🔄 DEVICE 상태 변경 → id={device_id}, status={new_status}") + + +# =============================== +# 메인 테스트 예시 +# =============================== +if __name__ == '__main__': + # DatabaseManager 클래스의 인스턴스를 생성합니다. + # 이 시점에 자동으로 __init__ 메서드가 실행되어 DB에 연결됩니다. + db = DBManager() + + # DB 연결이 성공했는지 확인 + if not db.conn or not db.conn.is_connected(): + print("DB 작업을 수행할 수 없습니다.") + exit() + + # 1. 데이터 삽입 (INSERT) + # print("\n--- 1. 데이터 삽입 예시 ---") + # new_device = { + # "device_id": "SEN003", + # "device_name": "거실 온도 센서", + # "type": "Sensor", + # "status": "active" + # } + # db.insert_data(table="devices", data= new_device) + + # 2. 데이터 조회 (SELECT) + print("\n--- 2. 데이터 조회 예시 ---") + # 'Sensor' 타입의 모든 장비 조회 + sensors = db.select_data(table="devices", where={"type": "LED"}) + print("조회된 센서 목록:") + for sensor in sensors: + print(f" - ID: {sensor['device_id']}, 이름: {sensor['name']}, 상태: {sensor['status']}") + + # 3. 데이터 수정 (UPDATE) + # print("\n--- 3. 데이터 수정 예시 ---") + # db.update_data( + # table="devices", + # data={"status": "inactive"}, + # where={"device_id": "SEN003"} + # ) + # # 수정 결과 확인 + # updated_sensor = db.select_data(table="devices", where={"device_id": "SEN003"}) + # print("수정된 센서 정보:", updated_sensor) + + # 4. 데이터 삭제 (DELETE) + # print("\n--- 4. 데이터 삭제 예시 ---") + # db.delete_data(table="devices", where={"device_id": "SEN003"}) \ No newline at end of file diff --git a/device_manager.py b/device_manager.py new file mode 100644 index 0000000..482d30e --- /dev/null +++ b/device_manager.py @@ -0,0 +1,117 @@ +from mqtt.mqtt_client import MqttClient +from devices.led import LED_Device +from devices.dht import DHT_Device +import time +import json + +class DeviceManager: + def __init__(self,client: MqttClient, office_id): + self.office_id = office_id # 보드 별로 층으로 구분해서 동작할 때 변경 + self.client = client + self.devices = {} + self.subscribe_list = {} # subscribe 해야할 디바이스들 여기에 + self.publish_list = {} # publish 통신하는 디바이스들 여기에 + + # MQTT 콜백 설정 + # 자바에서 publish 들어오면 _handle_mqtt_message 메서드가 실행됨 + self.client.set_on_message_callback(self._handle_mqtt_message) + # mqtt_client로 브로커 서버랑 연결하면 _on_mqtt_connect 메서드가 실행됨 + self.client.set_on_connect_callback(self._on_mqtt_connect) + + # publish 관련 로직 처리 + def add_publish(self,pub_id: str, device): + self.devices[pub_id] = device + self.publish_list[pub_id] = device + + def publish_data(self,device_id,data = None, action = "state"): + # publish 토픽, 메시지(payload)으로 데이터 전송 + # publish topic 구조: {officeId}/{decive_type}/{device_id}/state + device = self.devices[device_id] + topic = f"{self.office_id}/{device.get_type()}/{device_id}/{action}" + if data is None: + data = device.handle_mqtt_state() + self.client.publish(topic,data) + print(f"센서 데이터 발행 완료: {topic}") + print(f"발행 데이터: {data}") + return True + + # subscribe 관련 로직 처리 + def add_subscribe(self,sub_id: str, device): + self.subscribe_list[sub_id] = device + self.devices[sub_id] = device + # device 객체에 set_manager 메소드가 있는지 확인하고, 있다면 호출 + if hasattr(device, 'set_manager'): + print("set_manager") + device.set_manager(self,sub_id) + + def control_subscribe(self, target, command): + """subscribe 토픽들 처리""" + actuator = target + # 각 액추에이터 마다 handel_mqtt_command 메서드를 세팅해야한다. + if hasattr(actuator, 'handle_mqtt_command'): + hasReturn = actuator.handle_mqtt_command(command) + else: + print(f"액추에이터 제어 메서드가 없음") + return False + + def _handle_mqtt_message(self, topic: str, payload): + """MQTT 메시지 처리""" + """subscribe로 받은 메시지를 처리""" + print(f"MQTT 메시지 수신: {topic} -> {payload}") + + msg = json.loads(payload) + print(msg) + # 토픽 파싱 예시 + # {office_id}/{device_type}/{device_id}/cmd + topic_parts = topic.split('/') + + if len(topic_parts) >= 3: + office_id = topic_parts[0] + device_type = topic_parts[1] + device_id = topic_parts[2] + for sub_id, sub_obj in self.subscribe_list.items(): + # device_type이 동일한 디바이스를 찾기 + if sub_obj.type == device_type: + if sub_id == device_id: + self.control_subscribe(sub_obj,msg) + else: + print(f"알 수 없는 액추에이터: {device_id}") + #토픽 구분 문구가 2개 이하면 잘못된 토픽 형식 + else: + print(f"잘못된 토픽 형식: {topic}") + + def _on_mqtt_connect(self): + # connection 이후 subscribe 토픽 구독 + for sub_id, sub_obj in self.subscribe_list.items(): + topic = f"{self.office_id}/{sub_obj.get_type()}/{sub_id}/cmd" + print(f"토픽 구독: {topic}") + self.client.subscribe(topic,1) + + # DeviceManager 리소스 정리 + def cleanup(self): + try: + for device in self.subscribe_list.values(): + device.clear() + self.devices.clear() + self.publish_list.clear() + self.subscribe_list.clear() + + print("모든 디바이스 리소스 정리 완료") + + except Exception as e: + print(f"디바이스 정리 중 오류: {e}") + +# DeviceManager 클래스 메서드들 동작 잘 되나 임시 테스트하는거 +if __name__=="__main__": + dm = DeviceManager(MqttClient()) + dm.add_subscribe("23",LED_Device(23)) #pin_23 조명센서 추가 + time.sleep(1) + dm.add_publish("25",DHT_Device(25)) #pin_25 온습도센서 추가 + dm.client.connect() + time.sleep(1) + dm.publish_data("25") + time.sleep(1) + dm.control_subscribe("23",{"action": "led_on"}) + time.sleep(1) + dm.cleanup() + \ No newline at end of file diff --git a/devices/Ultrasonic.py b/devices/Ultrasonic.py new file mode 100644 index 0000000..efbc9fa --- /dev/null +++ b/devices/Ultrasonic.py @@ -0,0 +1,34 @@ +import RPi.GPIO as GPIO +import time + +class UltrasonicSensor: + def __init__(self, trig=23, echo=24): + self.trig = trig + self.echo = echo + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + GPIO.setup(self.trig, GPIO.OUT) + GPIO.setup(self.echo, GPIO.IN) + print(f"✅ 초음파 센서 초기화 완료 (TRIG={self.trig}, ECHO={self.echo})") + + def measure_distance(self): + """TRIG/ECHO를 이용해 거리(cm) 측정""" + GPIO.output(self.trig, True) + time.sleep(0.00001) + GPIO.output(self.trig, False) + + start = time.time() + end = time.time() + + while GPIO.input(self.echo) == 0: + start = time.time() + while GPIO.input(self.echo) == 1: + end = time.time() + + duration = end - start + distance = duration * 17150 + return round(distance, 2) + + def cleanup(self): + GPIO.cleanup([self.trig, self.echo]) + print("🧹 초음파 센서 핀 정리 완료") diff --git a/devices/dht.py b/devices/dht.py new file mode 100644 index 0000000..f6d1ea7 --- /dev/null +++ b/devices/dht.py @@ -0,0 +1,74 @@ +import time +import adafruit_dht +import board +import sys +import os + +class DHT_Device: + """DHT22 온습도 센서를 제어하는 클래스""" + + def __init__(self, pin: int): + self.type = "dht" + self.pin = getattr(board, f"D{pin}") + self.sensor = adafruit_dht.DHT11(self.pin) + print(f"DHT 초기화 성공, 핀 번호: {self.pin}") + self.last_temperature = None + self.last_humidity = None + + def handle_mqtt_state(self): + """온도와 습도 데이터 읽기""" + try: + temperature = self.sensor.temperature + humidity = self.sensor.humidity + data = { + "temperature": temperature, + "humidity": humidity + } + self.last_temperature = temperature + self.last_humidity = humidity + + print(f"센서 데이터: 온도 {temperature}°C, 습도 {humidity}%") + return data + + except RuntimeError as err: + print(err) + print(f"이전 센서 데이터: 온도 {self.last_temperature}°C, 습도 {self.last_humidity}%") + return { + "temperature": self.last_temperature, + "humidity": self.last_humidity + } + + def get_type(self): + return self.type + + def get_last_temperature(self): + """마지막으로 읽은 온도 반환""" + return self.last_temperature + + def get_last_humidity(self): + """마지막으로 읽은 습도 반환""" + return self.last_humidity + + def is_sensor_available(self) -> bool: + """센서 사용 가능 여부 확인""" + return self.sensor is not None + + def get_sensor_info(self): + """센서 정보 반환""" + return { + "pin": self.pin, + "available": self.is_sensor_available(), + "last_temperature": self.last_temperature, + "last_humidity": self.last_humidity, + } + +if __name__ == "__main__": + pin = 25 + sensor = DHT_Device(pin) + sensor.handle_mqtt_state() + time.sleep(1) + print(sensor.get_last_humidity()) + print(sensor.get_last_temperature()) + print(sensor.is_sensor_available()) + print(sensor.get_sensor_info()) + time.sleep(1) \ No newline at end of file diff --git a/devices/elevator.py b/devices/elevator.py new file mode 100644 index 0000000..d42e674 --- /dev/null +++ b/devices/elevator.py @@ -0,0 +1,169 @@ +#!/usr/bin/python3 +import RPi.GPIO as GPIO +import time +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from database.db_connect import DBManager + +# defining stepper motor sequence (found in documentation http://www.4tronix.co.uk/arduino/Stepper-Motors.php) +global step_sequence +step_sequence = [[1,1,0,0], # IN1, IN2 On + [0,1,1,0], # IN2, IN3 On + [0,0,1,1], # IN3, IN4 On + [1,0,0,1]] # IN4, IN1 On +# Full Step은 4단계이므로, 모터 회전 루프의 % 8을 % 4로 변경해야 합니다. + +class Elevator: + #엘리베이터는 스탭 모터를 기반으로 움직인다. + #스탭 모터는 회전 수에 따라 움직이는 거리가 결정되고, 층마다 해당 거리를 정해야한다. + + # 엘리베이터 관련 변수 초기화 + def __init__(self): + self.in1 = 12 + self.in2 = 16 + self.in3 = 20 + self.in4 = 21 + self.motor_pins = [self.in1, self.in2, self.in3, self.in4] + self.interval = 0.002 + self.type = "elevator" + self.last_floor = 1 + self.state = True + self.initialize() + + def initialize(self): + GPIO.setmode( GPIO.BCM ) + for pin in self.motor_pins: + GPIO.setup(pin,GPIO.OUT) + # initializing + for pin in self.motor_pins: + GPIO.output(pin,GPIO.LOW) + + # DB Connect + self.db = DBManager() + + # DB 연결이 성공했는지 check + if not self.db.conn or not self.db.conn.is_connected(): + print("DB 작업을 수행할 수 없습니다.") + return + + def turn(self,count,direction = False): # default : 시계 방향 + motor_step_counter = 0 + for i in range(int(count*4)): + for pin in range(0, len(self.motor_pins)): + GPIO.output(self.motor_pins[pin], step_sequence[motor_step_counter][pin] ) + time.sleep(self.interval) + if direction==True: # 반시계 회전, 층 올라가는 로직 + motor_step_counter = (motor_step_counter - 1) % 4 + elif direction==False: # 시계 회전, 층 내려가는 로직 + motor_step_counter = (motor_step_counter + 1) % 4 + + def turnDegrees(self,count,direction=False): + # Turn n degrees + self.turn(round(count*512/360,0),direction) + + # 모터 한바퀴를 도는 걸 1층으로 판단(임의코드) + # start는 항상 self.last_floor 가 되고, end는 사용자가 이용하려는 층이 된다. + # end 값은 1-3 범위에 있어야 한다. + def turnFloors(self,start,end): + if end < 1 or end > 3: + print("1-3 층만 이용 가능합니다.") + return False + count = end - start + if count == 0: #동일 층이면 동작 X + print("입력한 층은 현재 층입니다.") + return False + elif count > 0: # ex. start: 1 -> end: 3 + direction = True # 층을 올라가는 방향으로 회전 + else: # ex. start: 3 -> end: 2 + direction = False # 층을 내려가는 방향으로 회전 + count = -count + self.turnDegrees(360*5*count,direction) + return True + + def clear(self): + self.turnFloors(self.last_floor, 1) + GPIO.cleanup() + + def get_type(self): # 토픽을 생성하기 위함 + return self.type + + def set_manager(self, manager, id): + self.manager = manager + self.device_id = id + + def publish_state(self, action: str, start, end=None): + """엘리베이터의 현재 상태를 MQTT로 발행합니다.""" + if self.manager and self.device_id: + # 발행할 데이터 생성 + data = { + "from_floor": start, + "action": action + } + # DeviceManager의 publish_data 메소드 호출 + self.manager.publish_data(self.device_id, data) + + def handle_mqtt_command(self, command): + """JAVA에서 들어온 MQTT 명령 처리""" + action = command.get("action", "").lower() # {"action": "call"} 데이터일 경우 action = "call" + if action == "call": # 엘리베이터 제어 (층 이동) + if self.state is False: + print("엘리베이터를 현재 이용할 수 없습니다.") + return False + start_floor = int(command.get("start_floor")) + end_floor = int(command.get("end_floor")) + user_id = int(command.get("userId")) + + self.call_and_arrive(start_floor,end_floor,user_id) + elif action == "state_return": # 엘리베이터의 현재 상태를 요청하는 Mqtt 통신 + result = "enable" if self.state == True else "disable" + self.publish_state(result, self.last_floor) + elif action == "state_change": + self.state = bool(command.get("state")) + result = "enable" if self.state == True else "disable" + print("엘리베이터 상태 변경: ", result) + else: + print(f"알 수 없는 E/V 명령: {action}") + return False + def call_and_arrive(self,start,end,user_id): + #원래 엘리베이터 층 -> start 층 이동 + data = self.insert_db_log(start,end,"called",user_id) + self.db.insert_data("elevator_log",data) + if self.turnFloors(self.last_floor,start): + data = self.insert_db_log(self.last_floor,start,"arrived",user_id) + self.last_floor = start + self.db.insert_data("elevator_log",data) + print(f"{start}층에 도착했습니다.") + print("문이 열립니다.") + time.sleep(3) + print("문이 닫힙니다.") + # start 층 -> end 층 이동 + if self.turnFloors(start,end): + data = self.insert_db_log(start,end,"arrived",user_id) + self.last_floor = end + self.db.insert_data("elevator_log",data) + print(f"{end}층에 도착완료") + + def insert_db_log(self,start_floor,end_floor,status,user_id): + data = { + "from_floor": start_floor, + "to_floor": end_floor, + "status": status, + "user_id": user_id, + "device_id": self.device_id + } + return data + + +# the meat +if __name__ == "__main__": + try: + ev = Elevator() + print("1층 -> 3층 이동") + ev.turnFloors(1,3) + time.sleep(1) + ev.clear() + + except KeyboardInterrupt: + ev.clear() + exit( 1 ) \ No newline at end of file diff --git a/devices/lcd.py b/devices/lcd.py new file mode 100644 index 0000000..6201f3f --- /dev/null +++ b/devices/lcd.py @@ -0,0 +1,74 @@ +import smbus2 +import time +import RPi.GPIO as gpio + +button_pin = 21 + +gpio.setmode(gpio.BCM) +gpio.setup(button_pin, gpio.IN, pull_up_down=gpio.PUD_UP) + +# LCD 기본 설정 +I2C_ADDR = 0x27 # LCD 주소 (i2cdetect로 확인) +LCD_WIDTH = 16 # 문자 수 (16x2 LCD) +LCD_CHR = 1 +LCD_CMD = 0 + +LCD_LINE_1 = 0x80 # 1번째 줄 주소 +LCD_LINE_2 = 0xC0 # 2번째 줄 주소 + +LCD_BACKLIGHT = 0x08 # 백라이트 ON +ENABLE = 0b00000100 # Enable 비트 + +# I2C 버스 객체 +bus = smbus2.SMBus(1) +def lcd_byte(bits, mode): + """명령 또는 데이터 전송""" + bits_high = mode | (bits & 0xF0) | LCD_BACKLIGHT + bits_low = mode | ((bits << 4) & 0xF0) | LCD_BACKLIGHT + + bus.write_byte(I2C_ADDR, bits_high) + lcd_toggle_enable(bits_high) + bus.write_byte(I2C_ADDR, bits_low) + lcd_toggle_enable(bits_low) + +def lcd_toggle_enable(bits): + """Enable 펄스 주기""" + time.sleep(0.0005) + bus.write_byte(I2C_ADDR, (bits | ENABLE)) + time.sleep(0.0005) + bus.write_byte(I2C_ADDR, (bits & ~ENABLE)) + time.sleep(0.0005) + +def lcd_init(): + """LCD 초기화""" + lcd_byte(0x33, LCD_CMD) # 초기화 시퀀스 + lcd_byte(0x32, LCD_CMD) + lcd_byte(0x06, LCD_CMD) + lcd_byte(0x0C, LCD_CMD) # 디스플레이 ON + lcd_byte(0x28, LCD_CMD) # 4비트, 2라인, 5x8 폰트 + lcd_byte(0x01, LCD_CMD) # 화면 클리어 + time.sleep(0.005) + +def lcd_string(message, line): + """LCD에 문자열 표시""" + message = message.ljust(LCD_WIDTH, " ") + lcd_byte(line, LCD_CMD) + for i in range(LCD_WIDTH): + lcd_byte(ord(message[i]), LCD_CHR) + +# 메인 루프 +if __name__ == '__main__': + try: + while True: + lcd_init() + lcd_string("Warning!!", LCD_LINE_1) + lcd_string("Fire Outbreak!", LCD_LINE_2) + time.sleep(0.5) + + lcd_byte(0x01, LCD_CMD) + time.sleep(0.05) + except KeyboardInterrupt: + pass + finally: + lcd_byte(0x01, LCD_CMD) # LCD 클리어 + bus.close() \ No newline at end of file diff --git a/devices/led.py b/devices/led.py new file mode 100644 index 0000000..03acb5e --- /dev/null +++ b/devices/led.py @@ -0,0 +1,110 @@ +import RPi.GPIO as gpio +import time + +class LED_Device: + def __init__(self,pin: int): + self.type = "led" + self.pin = pin #핀 번호 + self.current_state = False # on/off state + self.brightness = 0 # 0-100 range 밝기 세기 + + #GPIO 핀 세팅 + try: + gpio.setmode(gpio.BCM) + gpio.setup(self.pin,gpio.OUT) + gpio.output(self.pin,gpio.LOW) + self.pwm = gpio.PWM(self.pin, 100) #주파수 100Hz 설정 + print(f"LED 초기화 성공, 핀 번호: {self.pin}") + except Exception as e: + print(f"LED 초기화에 문제 발생: {e}") + + # LED의 상태 제어 + def set_power(self,state: bool): + if state: + self.current_state = True + gpio.output(self.pin,gpio.HIGH) + elif not state: + self.current_state = False + + gpio.output(self.pin,gpio.LOW) + + # LED 밝기 조절 + def set_brightness(self,brightness: int = 100): + """LED 밝기 설정 (0-100)""" + try: + # 밝기 값 범위 확인 + brightness = max(0, min(100, brightness)) + + # PWM을 사용한 밝기 제어 + if not hasattr(self, 'pwm'): + self.pwm = gpio.PWM(self.pin, 100) # 100Hz 주파수 + self.pwm.start(0) + + self.pwm.ChangeDutyCycle(brightness) + + #밝기 조절을 0 이상으로 줬다는 건 on 상태라는 것 + self.brightness = brightness + self.current_state = brightness > 0 + if self.current_state: + print(f"LED 밝기 설정: {brightness}%") + gpio.output(self.pin, gpio.HIGH) + else: + print(f"LED 꺼짐") + gpio.output(self.pin, gpio.LOW) + + except Exception as e: + print(f"LED 밝기 설정 실패: {e}") + + def get_type(self): + """LED 타입 반환""" + return self.type + + def publish_state(self, action: str): + """LED의 현재 상태를 MQTT로 발행합니다.""" + if self.manager and self.device_id: + # 발행할 데이터 생성 + data = { + "action": action, + "pin": self.pin, + "state": self.current_state, + "brightness": self.brightness + } + # DeviceManager의 publish_data 메소드 호출 + self.manager.publish_data(self.device_id, data) + + def set_manager(self, manager, id): + self.manager = manager + self.device_id = id + + def handle_mqtt_command(self, command): + """MQTT 명령 처리""" + action = command.get("action", "").lower() + print(command, action) + if action == "led_on": + self.set_power(True) + elif action == "led_off": + self.set_power(False) + elif action == "brightness": + brightness = int(command.get("brightness")) + self.set_brightness(brightness) + elif action == "state_return": + self.publish_state(action,) + return self.get_state() + else: + print(f"알 수 없는 LED 명령: {action}") + return False + + def clear(self): + gpio.output(self.pin,gpio.LOW) + +if __name__ == "__main__": + led = LED_Device(23) + led.set_power(True) + print(led.get_state()) + time.sleep(1) + led.set_brightness(50) + print(led.get_state()) + time.sleep(1) + led.set_power(False) + print(led.get_state()) + \ No newline at end of file diff --git a/devices/servo.py b/devices/servo.py new file mode 100644 index 0000000..11ad77f --- /dev/null +++ b/devices/servo.py @@ -0,0 +1,44 @@ +import RPi.GPIO as GPIO +import time +from threading import Thread + +class ServoGate: + def __init__(self, pin=18): + self.pin = pin + GPIO.setwarnings(False) + GPIO.setmode(GPIO.BCM) + GPIO.setup(self.pin, GPIO.OUT) + + # ✅ PWM 객체를 한 번만 생성해서 재사용 + self.pwm = GPIO.PWM(self.pin, 50) # 50Hz 주파수 + self.pwm.start(0) + print(f"✅ 서보모터 초기화 완료 (핀 {self.pin})") + + def set_angle(self, angle): + """각도를 DutyCycle로 변환해 이동""" + duty = 2.5 + (angle / 18) # 0~180도 범위 + print(f"⚙️ 이동: {angle}° (Duty={duty:.2f})") + self.pwm.ChangeDutyCycle(duty) + time.sleep(1.5) # 서보가 물리적으로 움직일 시간 확보 + self.pwm.ChangeDutyCycle(0) # 떨림 방지 + + def open_gate(self): + print("🔓 차단기 열림") + self.set_angle(90) + print("✅ 열림 완료") + + def close_gate(self): + print("🔒 차단기 닫힘") + self.set_angle(0) + print("✅ 닫힘 완료") + + def open_async(self): + Thread(target=self.open_gate, daemon=True).start() + + def close_async(self): + Thread(target=self.close_gate, daemon=True).start() + + def cleanup(self): + self.pwm.stop() + GPIO.cleanup(self.pin) + print("🧹 서보모터 핀 정리 완료") diff --git a/devices/sumin.py b/devices/sumin.py new file mode 100644 index 0000000..4b135d4 --- /dev/null +++ b/devices/sumin.py @@ -0,0 +1,120 @@ +import RPi.GPIO as GPIO +from mfrc522 import SimpleMFRC522 +import pymysql +import time + +# =============================== +# 🔧 환경 설정 +# =============================== +SERVO_PIN = 11 +OPEN_DC = 7.5 +CLOSE_DC = 2.5 +OPEN_DURATION = 3 # 문 열려있는 시간 + +# mode = GPIO.getmode() +# if mode == GPIO.BOARD: +# SERVO_PIN = 17 # 주황색 선이 물리 핀 17에 꽂혀 있음 +# elif mode == GPIO.BCM: +# SERVO_PIN = 0 # BCM에서 핀17은 존재하지 않음 (사용 불가) +# else: +# raise RuntimeError("GPIO mode not set properly.") +# GPIO.setup(SERVO_PIN, GPIO.OUT) +# servo = GPIO.PWM(SERVO_PIN, 50) +# servo.start(CLOSE_DC) + + +DB_CONFIG = { + "host": "192.168.14.74", + "user": "sample", + "password": "tnalsdlchlrhdi!", + "db": "smartbuilding4", + "charset": "utf8mb4", + "cursorclass": pymysql.cursors.DictCursor +} + +# =============================== +# 🧠 함수 정의 +# =============================== +reader = SimpleMFRC522() +GPIO.setup(SERVO_PIN, GPIO.OUT) +servo = GPIO.PWM(SERVO_PIN, 50) +servo.start(CLOSE_DC) + +def db_connect(): + return pymysql.connect(**DB_CONFIG) + +def is_member_card(card_id): + """member 계정의 card_id가 맞는지 DB에서 확인""" + try: + conn = db_connect() + with conn.cursor() as cur: + sql = """ + SELECT name, id, is_active + FROM users + WHERE card_id = 'memberCard' + LIMIT 1 + """ + cur.execute(sql) + row = cur.fetchone() + conn.close() + if row and int(row["is_active"]) == 1: + return True, row["name"] + else: + return False, None + except Exception as e: + print("❌ DB 오류:", e) + return False, None + +def open_door(): + print("🔓 문이 열립니다.") + servo.ChangeDutyCycle(OPEN_DC) + time.sleep(0.5) + servo.ChangeDutyCycle(0) + time.sleep(OPEN_DURATION) + close_door() + +def close_door(): + print("🔒 문이 닫힙니다.") + servo.ChangeDutyCycle(CLOSE_DC) + time.sleep(0.5) + servo.ChangeDutyCycle(0) +def log_event(card_id, user_name, success): + """출입 시도 이벤트 로그 기록""" + try: + conn = db_connect() + with conn.cursor() as cur: + sql = """ + INSERT INTO event_log (card_id, user_name, event_time, success) + VALUES (%s, %s, NOW(), %s) + """ + cur.execute(sql, (card_id, user_name, int(success))) + conn.commit() + except Exception as e: + print("⚠️ 로그 기록 중 오류:", e) + finally: + conn.close() +# =============================== +# 🚪 메인 루프 +# =============================== +try: + print("===================================") + print(" RFID 카드를 리더기에 대주세요...") + print("===================================") + + while True: + card_id, text = reader.read() + print(f"\n[RFID 감지됨] ID={card_id}") + + authorized, user_name = is_member_card(card_id) + if authorized: + print(f"✅ {user_name} 님 (member 계정) → 출입 허가됨") + open_door() + else: + print("🚫 시연용 memberCard가 아닙니다. 문을 열지 않습니다.") + time.sleep(1) + +except KeyboardInterrupt: + print("\n🛑 종료합니다.") +finally: + servo.stop() + GPIO.cleanup() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..4873dbe --- /dev/null +++ b/main.py @@ -0,0 +1,97 @@ +from mqtt.mqtt_client import MqttClient +from device_manager import DeviceManager +from devices.dht import DHT_Device +from devices.led import LED_Device +from devices.elevator import Elevator +import time + +def main(): + print("IoT Raspberry Pi 시스템 시작") + global mqtt, device_manager + mqtt = MqttClient() + office_id = 1 # 해당 officeID 값 Mqtt 통신할 때 토픽으로 받는 값이 + # {office_id}/{device_type}{device_pin}/cmd + device_manager = DeviceManager(mqtt,office_id) + + # 디바이스 초기화 + + # DHT 온습도 센서 추가 (주로 publish) + # dht_pin = 25 + # dht_sensor = DHT_Device(dht_pin) + # device_manager.add_publish(str(dht_pin), dht_sensor) + # print("DHT 센서 초기화 완료") + + # LED 액추에이터 추가 (주로 subscribe) + # led_pin = 23 + # led_actuator = LED_Device(led_pin) + # device_manager.add_subscribe(str(led_pin), led_actuator) + # print("LED 액추에이터 초기화 완료") + + elevator = Elevator() + device_manager.add_subscribe("1",elevator) + print("엘리베이터 초기화 완료") + +def connect(): + #Mqtt 연결 + mqtt.connect() + print("MQTT 브로커 서버 접속 시도") + time.sleep(1) + if not mqtt.connected: + print("MQTT 연결실패") + return False + else: + return True + +def main_loop(): + """메인 실행 루프""" + sensor_interval = 5.0 # 센서 데이터 수집 간격 (초) + last_sensor_time = 0 + + while True: + current_time = time.time() + + # 센서 데이터 수집 및 발행 + if current_time - last_sensor_time >= sensor_interval: + devices = device_manager.publish_list + for device in devices.keys(): + device_manager.publish_data(device) + last_sensor_time = current_time + + # MQTT 연결 상태 확인 + if not mqtt.connected: + print("MQTT 연결 끊어짐 - 재연결 시도") + try: + mqtt.connect() + time.sleep(1) + except Exception as e: + print(f"MQTT 재연결 실패: {e}") + + # 짧은 대기 + time.sleep(0.1) + +def stop(): + print("IoT 시스템 중지") + try: + # 디바이스 정리 + if device_manager: + device_manager.cleanup() + + # MQTT 연결 해제 + if mqtt: + mqtt.disconnect() + except Exception as e: + print(f"시스템 중지 중 오류: {e}") + finally: + print("IoT 시스템 중지 완료") + +if __name__ == "__main__": + main() + isConnected = connect() + if isConnected: + try: + main_loop() + except KeyboardInterrupt: + print("\n시스템이 사용자에 의해 중단되었습니다.") + stop() + except Exception as e: + print(f"시스템 오류: {e}") \ No newline at end of file diff --git a/main_parking.py b/main_parking.py new file mode 100644 index 0000000..3116e71 --- /dev/null +++ b/main_parking.py @@ -0,0 +1,169 @@ +import time, json, random +from datetime import datetime +from threading import Thread +import paho.mqtt.client as mqtt +from dotenv import load_dotenv +from devices.Ultrasonic import UltrasonicSensor +from devices.servo import ServoGate +from database.db_connect import DBManager +import os + +# ============================== +# 환경 변수 로드 +# ============================== +load_dotenv() +BROKER = os.getenv("BROKER_HOST") +PORT = int(os.getenv("BROKER_PORT")) +TOPIC_CMD = os.getenv("TOPIC_CMD") +TOPIC_CAR = os.getenv("TOPIC_CAR") + +# ============================== +# 디바이스 초기화 +# ============================== +sensor = UltrasonicSensor() +servo = ServoGate() +db = DBManager() # ✅ DB 연결 +sensor_active = False + +# ============================== +# DB 차량 목록 가져오기 +# ============================== +def get_registered_vehicles(): + try: + vehicles = db.select_data("users", columns="user_id, vehicle_no") + car_dict = {row["vehicle_no"]: row["user_id"] for row in vehicles if row["vehicle_no"]} + print(f"🚘 DB 등록 차량 목록: {list(car_dict.keys())}") + return car_dict + except Exception as e: + print(f"⚠️ 차량목록 불러오기 오류: {e}") + return {} + +# ============================== +# parking_log 테이블에 기록 +# ============================== +def log_parking_event(user_id, car_no): + try: + # 1️⃣ 현재 차량이 아직 출차되지 않은 상태인지 확인 + query = f"SELECT parking_id, action FROM parking_log WHERE user_id = %s AND action = 'IN' ORDER BY in_time DESC LIMIT 1" + cur = db.conn.cursor(dictionary=True) + cur.execute(query, (user_id,)) + record = cur.fetchone() + cur.close() + + # 2️⃣ 현재 주차 중이면 (IN 상태) → OUT으로 업데이트 + if record: + sql = """ + UPDATE parking_log + SET out_time = %s, action = 'OUT', note = %s + WHERE parking_id = %s + """ + note = f"등록 차량 출차 ({car_no})" + cur = db.conn.cursor() + cur.execute(sql, (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), note, record["parking_id"])) + db.conn.commit() + cur.close() + print(f"🚗 차량 출차 기록 업데이트 완료 → {car_no}") + + + + except Exception as e: + print(f"⚠️ 주차 로그 처리 오류: {e}") + +# ============================== +# 초음파 센서 루프 +# ============================== +def sensor_loop(): + global sensor_active + print("📡 초음파 센서 루프 시작") + + registered_cars = get_registered_vehicles() + + while sensor_active: + try: + distance = sensor.measure_distance() + print(f"📏 거리: {distance:.2f} cm") + + if 0 < distance < 10: + # 70% 등록 차량 / 30% 미등록 차량 + if random.random() < 0.7 and registered_cars: + car_no = random.choice(list(registered_cars.keys())) + user_id = registered_cars[car_no] + else: + car_no = f"{random.randint(100,999)}가{random.randint(1000,9999)}" + user_id = None + + print(f"🚗 차량 감지 → {car_no}") + + # ✅ 등록 차량 + if user_id: + print(f"✅ 등록 차량 확인: {car_no}") + servo.open_async() + client.publish(TOPIC_CAR, json.dumps({ + "carNo": car_no, + "action": "authorized" + }, ensure_ascii=False)) + log_parking_event(user_id, car_no) # ✅ 이 한 줄로 변경 + print("🔓 차단기 열림") + + + # ❌ 미등록 차량 + else: + print(f"🚫 미등록 차량: {car_no}") + servo.close_async() + message = { + "carNo": car_no, + "action": "unauthorized", + "note": f"미등록 차량 접근 ({car_no})" + } + client.publish(TOPIC_CAR, json.dumps(message, ensure_ascii=False)) + log_parking_event(0, "DENY", message["note"]) + print("🔒 차단기 닫힘 유지") + + time.sleep(5) + time.sleep(1.5) + + except Exception as e: + print(f"⚠️ 센서 루프 오류: {e}") + time.sleep(1) + +# ============================== +# MQTT 콜백 +# ============================== +def on_connect(client, userdata, flags, rc): + if rc == 0: + print("✅ MQTT 브로커 연결 성공") + client.subscribe(TOPIC_CMD) + print(f"📡 구독 완료 → {TOPIC_CMD}") + else: + print(f"❌ 연결 실패 (코드: {rc})") + +def on_message(client, userdata, msg): + global sensor_active + payload = msg.payload.decode("utf-8") + print(f"📩 수신 → {msg.topic}: {payload}") + + try: + data = json.loads(payload) + if data.get("action") == "activate": + if not sensor_active: + sensor_active = True + print("🚗 센서 활성화 명령 수신") + Thread(target=sensor_loop, daemon=True).start() + except Exception as e: + print(f"⚠️ 메시지 처리 오류: {e}") + +# ============================== +# 메인 실행 +# ============================== +client = mqtt.Client("pi_parking_client") +client.on_connect = on_connect +client.on_message = on_message +client.connect(BROKER, PORT, 60) + +try: + print("🚀 스마트 주차 시스템 실행 중...") + client.loop_forever() +except KeyboardInterrupt: + print("🛑 프로그램 종료") + sensor.cleanup() + servo.cleanup() diff --git a/mqtt/mqtt_client.py b/mqtt/mqtt_client.py new file mode 100644 index 0000000..7d220be --- /dev/null +++ b/mqtt/mqtt_client.py @@ -0,0 +1,109 @@ +import time +import os +from dotenv import load_dotenv +import paho.mqtt.client as mqtt +from threading import Thread + +class MqttClient: + # 브로커 서버 값 할당 (ip,port,id) + def __init__(self,client_id: str = "raspberry_pi"): + env_path = os.path.join(os.path.dirname(__file__), '..', '.env') + load_dotenv(dotenv_path=env_path) + + self.broker_host = os.getenv("BROKER_HOST") + self.broker_port = int(os.getenv("BROKER_PORT")) # 포트는 정수형 + self.client_id = client_id + self.client = None + self.connected = False + + # 콜백 함수들 + self.on_message_callback = None + self.on_connect_callback = None + self.on_disconnect_callback = None + + ###### 브로커 서버 연결, 연결 콜백함수 ##### + + # 브로커 서버 연결 + def connect(self): + self.client = mqtt.Client(client_id=self.client_id) + self.client.on_connect = self.on_connect + self.client.on_message = self.on_message + + self.client.connect(self.broker_host, self.broker_port, 60) + mqtt_obj = Thread(target=self.client.loop_forever) + mqtt_obj.start() + print(f"MQTT 클라이언트 연결 시도: {self.broker_host}:{self.broker_port}") + # MQTT 연결 콜백 (connect() 메서드 실행되면 자동 콜백되는 메서드) + def on_connect(self, client, userdata, flags, rc): + if rc == 0: + self.connected = True + print("MQTT 브로커 연결 성공") + if self.on_connect_callback: + self.on_connect_callback() + else: + self.connected = False + print(f"MQTT 브로커 연결 실패: {rc}") + # device_manager에서 on_connect_callback 메서드를 할당 + # connect() 실행 시 할당된 메서드도 실행되도록 + def set_on_connect_callback(self, callback): + self.on_connect_callback = callback + + ######################################### + + # subscribe 메시지 콜백 + def on_message(self, client, userdata, msg): + topic = msg.topic + value = msg.payload.decode("utf-8") + print(topic + "===========" + value) + if self.on_message_callback: + self.on_message_callback(topic,value) + + def set_on_message_callback(self, callback): + """메시지 수신 콜백 함수 설정""" + self.on_message_callback = callback + + def subscribe(self, topic: str, qos: int = 1): + """토픽 구독""" + if not self.connected: + print("MQTT 연결되지 않음. 구독 실패") + return False + self.client.subscribe(topic, qos) + + def publish(self, topic, payload: dict, qos:int = 1): + """토픽에 메시지 발행""" + if not self.connected: + print("MQTT 연결되지 않음. 메시지 발행 실패") + return False + message = str(payload) + self.client.publish(topic,message, qos=qos) + + def disconnect(self): + """MQTT 브로커 연결 해제""" + if self.client: + self.client.loop_stop() + self.client.disconnect() + self.connected = False + print("MQTT 연결 해제") + + # MqttClient 객체의 상태 정보 반환 + def get_state(self): + """MQTT 현재 상태 반환""" + return { + "client_id": self.client_id, + "connected": self.connected, + } + +if __name__ == "__main__": + mqtt_obj = MqttClient() + time.sleep(1) + mqtt_obj.connect() + time.sleep(1) + mqtt_obj.subscribe("1/led/23/cmd",0) + time.sleep(2) + mqtt_obj.publish("1/led/23/cmd",{"control":"led_on"}) + time.sleep(1) + print(mqtt_obj.get_state()) + mqtt_obj.disconnect() + time.sleep(1) + print(mqtt_obj.get_state()) + \ No newline at end of file diff --git a/pi#1.py b/pi#1.py new file mode 100644 index 0000000..05f2e70 --- /dev/null +++ b/pi#1.py @@ -0,0 +1,122 @@ +# Pi #1 (1층 + 주차장) | IP: 192.168.14. +# ┌─────────────────────────────┐ +# │ LED 조명(1층) Pin 23 PWM +# │ 서보모터(1층) Pin 24 PWM +# │ DHT11 센서 Pin 25 1-Wire +# │ +# │ [주차장] +# │ 초음파 TRIG Pin 5 GPIO +# │ 초음파 ECHO Pin 6 GPIO +# │ 서보모터 Pin 26 PWM +# └─────────────────────────────┘ + +from mqtt.mqtt_client import MqttClient +from device_manager import DeviceManager +from devices.dht import DHT_Device +from devices.led import LED_Device +from devices.elevator import Elevator +import time + +def main(): + print(""" + # Pi #1 (1층 + 주차장) r4 +# ┌─────────────────────────────┐ +# │ LED 조명(1층) Pin 23 PWM +# │ 서보모터(1층) Pin 24 PWM +# │ DHT11 센서 Pin 25 1-Wire +# │ +# │ [주차장] +# │ 초음파 TRIG Pin 5 GPIO +# │ 초음파 ECHO Pin 6 GPIO +# │ 서보모터 Pin 26 PWM +# └─────────────────────────────┘ + """) + global mqtt, device_manager + mqtt = MqttClient() + device_manager = DeviceManager(mqtt) + + # 디바이스 초기화 + # LED 액추에이터 + led_pin = 23 + led_actuator = LED_Device(led_pin) + device_manager.add_subscribe(str(led_pin), led_actuator) + print("LED 액추에이터 초기화 완료") + # 서보모터 ??? + servo_pin =24 + # 서보모터 생성자 생성 + # device_manager.add_subscribe() 추가 + + # DHT 온습도 센서 + dht_pin = 25 + dht_sensor = DHT_Device(dht_pin) + device_manager.add_publish(str(dht_pin), dht_sensor) + print("DHT 센서 초기화 완료") + + elevator = Elevator() + device_manager.add_subscribe("4",elevator) + print("엘리베이터 초기화 완료") + +def connect(): + #Mqtt 연결 + mqtt.connect() + print("MQTT 브로커 서버 접속 시도") + time.sleep(1) + if not mqtt.connected: + print("MQTT 연결실패") + return False + else: + return True + +def main_loop(): + """메인 실행 루프""" + sensor_interval = 5.0 # 센서 데이터 수집 간격 (초) + last_sensor_time = 0 + + while True: + current_time = time.time() + + # 센서 데이터 수집 및 발행 + if current_time - last_sensor_time >= sensor_interval: + devices = device_manager.publish_list + for device in devices.keys(): + device_manager.publish_data(device) + last_sensor_time = current_time + + # MQTT 연결 상태 확인 + if not mqtt.connected: + print("MQTT 연결 끊어짐 - 재연결 시도") + try: + mqtt.connect() + time.sleep(1) + except Exception as e: + print(f"MQTT 재연결 실패: {e}") + + # 짧은 대기 + time.sleep(0.1) + +def stop(): + print("IoT 시스템 중지") + try: + # 디바이스 정리 + if device_manager: + device_manager.cleanup() + + # MQTT 연결 해제 + if mqtt: + mqtt.disconnect() + except Exception as e: + print(f"시스템 중지 중 오류: {e}") + finally: + print("IoT 시스템 중지 완료") + +if __name__ == "__main__": + main() + isConnected = connect() + if isConnected: + try: + main_loop() + except KeyboardInterrupt: + print("\n시스템이 사용자에 의해 중단되었습니다.") + stop() + except Exception as e: + print(f"시스템 오류: {e}") \ No newline at end of file diff --git a/pi#2.py b/pi#2.py new file mode 100644 index 0000000..f7c2286 --- /dev/null +++ b/pi#2.py @@ -0,0 +1,109 @@ +# Pi #2 (2층.3층 + 화재경보) | IP: 192.168.14. +# ┌─────────────────────────────┐ +# │ LED 조명(2층) Pin 23 PWM +# │ 경보장치 Pin 22 PWM +# │ LED 조명(3층) Pin 26 PWM +# │ 서보모터(2층) Pin 24 PWM +# │ 화재센서(MQ-2) Pin 27 INPUT +# │ 서보모터(3층) Pin 4 PWM +# └─────────────────────────────┘ + +from mqtt.mqtt_client import MqttClient +from device_manager import DeviceManager +from devices.dht import DHT_Device +from devices.led import LED_Device +from devices.elevator import Elevator +import time + +def main(): + print("IoT Raspberry Pi 시스템 시작") + global mqtt, device_manager + mqtt = MqttClient() + # 해당 officeID 값 Mqtt 통신할 때 토픽으로 받는 값이 + # {office_id}/{device_type}{device_pin}/cmd + # 여기서는 2층과 3층을 제어해야댐 + office_id = 1 + device_manager = DeviceManager(mqtt,office_id) + + # 디바이스 초기화 + + # DHT 온습도 센서 추가 (주로 publish) + # dht_pin = 25 + # dht_sensor = DHT_Device(dht_pin) + # device_manager.add_publish(str(dht_pin), dht_sensor) + # print("DHT 센서 초기화 완료") + + # LED 액추에이터 추가 (주로 subscribe) + led_pin1 = 23 + led_actuator1 = LED_Device(led_pin1) + device_manager.add_subscribe(str(led_pin1), led_actuator1) + print("LED 액추에이터 초기화 완료") + led_pin2 = 26 + led_actuator2 = LED_Device(led_pin2) + device_manager.add_subscribe(str(led_pin2), led_actuator2) + print("LED 액추에이터 초기화 완료") + +def connect(): + #Mqtt 연결 + mqtt.connect() + print("MQTT 브로커 서버 접속 시도") + time.sleep(1) + if not mqtt.connected: + print("MQTT 연결실패") + return False + else: + return True + +def main_loop(): + """메인 실행 루프""" + sensor_interval = 5.0 # 센서 데이터 수집 간격 (초) + last_sensor_time = 0 + + while True: + current_time = time.time() + + # 센서 데이터 수집 및 발행 + if current_time - last_sensor_time >= sensor_interval: + devices = device_manager.publish_list + for device in devices.keys(): + device_manager.publish_data(device) + last_sensor_time = current_time + + # MQTT 연결 상태 확인 + if not mqtt.connected: + print("MQTT 연결 끊어짐 - 재연결 시도") + try: + mqtt.connect() + time.sleep(1) + except Exception as e: + print(f"MQTT 재연결 실패: {e}") + + # 짧은 대기 + time.sleep(0.1) + +def stop(): + print("IoT 시스템 중지") + try: + # 디바이스 정리 + if device_manager: + device_manager.cleanup() + + # MQTT 연결 해제 + if mqtt: + mqtt.disconnect() + except Exception as e: + print(f"시스템 중지 중 오류: {e}") + finally: + print("IoT 시스템 중지 완료") + +if __name__ == "__main__": + main() + isConnected = connect() + if isConnected: + try: + main_loop() + except KeyboardInterrupt: + print("\n시스템이 사용자에 의해 중단되었습니다.") + stop() + except Exception as e: + print(f"시스템 오류: {e}") \ No newline at end of file diff --git a/pi#3.py b/pi#3.py new file mode 100644 index 0000000..7153949 --- /dev/null +++ b/pi#3.py @@ -0,0 +1,91 @@ +# Pi #3 (건물 외부 - 엘리베이터) | IP: 192.168.14.** +# ┌─────────────────────────────┐ +# │ 스텝모터 IN1 Pin 12 GPIO +# │ 스텝모터 IN2 Pin 16 GPIO +# │ 스텝모터 IN3 Pin 20 GPIO +# │ 스텝모터 IN4 Pin 21 GPIO +# └─────────────────────────────┘ + +from mqtt.mqtt_client import MqttClient +from device_manager import DeviceManager +from devices.elevator import Elevator +import time + +def main(): + print("IoT Raspberry Pi 시스템 시작") + global mqtt, device_manager + mqtt = MqttClient() + office_id = 1 # 해당 officeID 값 Mqtt 통신할 때 토픽으로 받는 값이 + # {office_id}/{device_type}{device_pin}/cmd + device_manager = DeviceManager(mqtt,office_id) + + # 디바이스 초기화 + + elevator = Elevator() + device_manager.add_subscribe("1",elevator) + print("엘리베이터 초기화 완료") + +def connect(): + #Mqtt 연결 + mqtt.connect() + print("MQTT 브로커 서버 접속 시도") + time.sleep(1) + if not mqtt.connected: + print("MQTT 연결실패") + return False + else: + return True + +def main_loop(): + """메인 실행 루프""" + sensor_interval = 5.0 # 센서 데이터 수집 간격 (초) + last_sensor_time = 0 + + while True: + current_time = time.time() + + # 센서 데이터 수집 및 발행 + if current_time - last_sensor_time >= sensor_interval: + devices = device_manager.publish_list + for device in devices.keys(): + device_manager.publish_data(device) + last_sensor_time = current_time + + # MQTT 연결 상태 확인 + if not mqtt.connected: + print("MQTT 연결 끊어짐 - 재연결 시도") + try: + mqtt.connect() + time.sleep(1) + except Exception as e: + print(f"MQTT 재연결 실패: {e}") + + # 짧은 대기 + time.sleep(0.1) + +def stop(): + print("IoT 시스템 중지") + try: + # 디바이스 정리 + if device_manager: + device_manager.cleanup() + + # MQTT 연결 해제 + if mqtt: + mqtt.disconnect() + except Exception as e: + print(f"시스템 중지 중 오류: {e}") + finally: + print("IoT 시스템 중지 완료") + +if __name__ == "__main__": + main() + isConnected = connect() + if isConnected: + try: + main_loop() + except KeyboardInterrupt: + print("\n시스템이 사용자에 의해 중단되었습니다.") + stop() + except Exception as e: + print(f"시스템 오류: {e}") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d81dcdf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +mysql-connector-python +python-dotenv +smbus2 # LCD 사용 라이브러리 \ No newline at end of file diff --git a/test/mq2_r0.json b/test/mq2_r0.json new file mode 100644 index 0000000..11250c0 --- /dev/null +++ b/test/mq2_r0.json @@ -0,0 +1,3 @@ +{ + "R0_ohm": 1554.5412363465725 +} \ No newline at end of file diff --git a/test/testTT..py b/test/testTT..py new file mode 100644 index 0000000..432474d --- /dev/null +++ b/test/testTT..py @@ -0,0 +1,168 @@ +import time, json, math, os +import board, busio, digitalio +from adafruit_mcp3xxx.mcp3008 import MCP3008, P0 +from adafruit_mcp3xxx.analog_in import AnalogIn +import adafruit_dht +import mysql.connector +from mysql.connector import Error +import paho.mqtt.publish as publish + +# =============================== +# DB 설정 +# =============================== +DB = { + "host": "192.168.14.74", + "user": "sample", + "password": "tnalsdlchlrhdi!", + "database": "smartbuilding4" +} + +# =============================== +# 장치 매핑 +# =============================== +DEVICE_DHT = 7 # DHT-202B +DEVICE_MQ2 = 10 # MQ2-202B +DEVICE_HVAC = 9 # HVAC-202B +OFFICE_ID = 1 # 202B 오피스 +USER_ID = None # 자동 발생 이벤트 + +# =============================== +# 센서 및 임계값 설정 +# =============================== +VCC = 5.0 +RL_KOHM = 5.0 +R0_PATH = "/home/pi/iot_raspberryPi/test/mq2_r0.json" +GAS_THRESHOLD = 500.0 +TEMP_THRESHOLD = 30.0 +HUMIDITY_LOW = 25.0 +DHT_PIN = board.D25 +dht = adafruit_dht.DHT11(DHT_PIN) + +# =============================== +# 공통 함수 +# =============================== +def connect_db(): + try: + conn = mysql.connector.connect(**DB) + if conn.is_connected(): + return conn + except Error as e: + print(f"❌ DB 연결 실패: {e}") + return None + +def load_r0(path): + with open(path) as f: + return json.load(f)["R0_ohm"] + +def rs_from_voltage(vout): + rl = RL_KOHM * 1000.0 + return rl * (VCC - vout) / vout if vout > 0 else math.inf + +def ppm_from_ratio(ratio): + p = {"x1": 200, "y1": 1.7, "x2": 10000, "y2": -0.38} + X1, Y1 = math.log10(p["x1"]), p["y1"] + X2, Y2 = math.log10(p["x2"]), p["y2"] + m = (Y2 - Y1) / (X2 - X1) + X = (math.log10(ratio) - Y1) / m + X1 + return 10 ** X + +# =============================== +# DB 조작 함수 +# =============================== +def insert_environment(conn, device_id, temp=None, hum=None, gas=None): + sql = """ + INSERT INTO environment_data (device_id, temperature, humidity, gas_level) + VALUES (%s, %s, %s, %s) + """ + cur = conn.cursor() + cur.execute(sql, (device_id, temp, hum, gas)) + conn.commit() + cur.close() + +def insert_event(conn, device_id, office_id, etype, action, value, note): + sql = """ + INSERT INTO event_log (device_id, user_id, office_id, event_type, event_action, value, note) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """ + cur = conn.cursor() + cur.execute(sql, (device_id, USER_ID, office_id, etype, action, value, note)) + conn.commit() + cur.close() + print(f"🚨 EVENT_LOG → [{etype}] {action} ({note})") + +def update_device_status(conn, device_id, new_status): + sql = "UPDATE devices SET status=%s, last_updated=NOW() WHERE device_id=%s" + cur = conn.cursor() + cur.execute(sql, (new_status, device_id)) + conn.commit() + cur.close() + print(f"🔄 DEVICE 상태 변경 → id={device_id}, status={new_status}") + +# =============================== +# 메인 루프 +# =============================== +def main(): + spi = busio.SPI(clock=board.SCK, MISO=board.MISO, MOSI=board.MOSI) + cs = digitalio.DigitalInOut(board.D8) + mcp = MCP3008(spi, cs) + ain = AnalogIn(mcp, P0) + R0 = load_r0(R0_PATH) + + conn = connect_db() + if not conn: + print("DB 연결 실패, 종료.") + return + + print("🌡️ 환경 모니터링 시작 (5초 간격)") + try: + while True: + try: + temp = dht.temperature + hum = dht.humidity + vout = ain.voltage + rs = rs_from_voltage(vout) + ratio = rs / R0 + gas_ppm = ppm_from_ratio(ratio) + + print(f"T={temp:.1f}°C | H={hum:.1f}% | GAS={gas_ppm:.1f}ppm") + + insert_environment(conn, DEVICE_DHT, temp, hum, None) + insert_environment(conn, DEVICE_MQ2, None, None, gas_ppm) + + # 이벤트 감지 + if gas_ppm > GAS_THRESHOLD: + insert_event(conn, DEVICE_MQ2, OFFICE_ID, "FIRE", "ALERT", f"{gas_ppm:.1f}", "가스농도 초과") + BROKER_IP = "192.168.14.74" + TOPIC_GAS = "building/gas" + update_device_status(conn, DEVICE_MQ2, "ALERT") + try: + publish.single(TOPIC_GAS, payload="ALERT", hostname=BROKER_IP) + print("📡 MQTT 발행 완료 → building/gas : ALERT") + except Exception as e: + print(f"⚠️ MQTT 발행 실패: {e}") + else: + update_device_status(conn, DEVICE_MQ2, "IDLE") + + if temp > TEMP_THRESHOLD: + insert_event(conn, DEVICE_DHT, OFFICE_ID, "HVAC", "ON", f"{temp:.1f}", "냉방 작동") + update_device_status(conn, DEVICE_HVAC, "ON") + elif hum < HUMIDITY_LOW: + insert_event(conn, DEVICE_DHT, OFFICE_ID, "HVAC", "ON", f"{hum:.1f}", "가습 작동") + update_device_status(conn, DEVICE_HVAC, "ON") + else: + update_device_status(conn, DEVICE_HVAC, "IDLE") + + except RuntimeError as e: + print(f"⚠️ 센서 읽기 오류: {e}") + + time.sleep(5) + + except KeyboardInterrupt: + print("\n🛑 사용자 중단") + finally: + dht.exit() + conn.close() + print("🔒 DB 연결 종료") + +if __name__ == "__main__": + main() \ No newline at end of file