초기화
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
from datetime import datetime
|
||||
from database.connection import get_db_connection
|
||||
import mysql.connector
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- 3단계 핵심: plc_mapping.json 매핑 설정 로드 ---
|
||||
MAPPING_FILE_PATH = os.path.join(os.path.dirname(__file__), "plc_mapping.json")
|
||||
plc_mapping = {}
|
||||
|
||||
def load_plc_mapping():
|
||||
global plc_mapping
|
||||
try:
|
||||
if os.path.exists(MAPPING_FILE_PATH):
|
||||
with open(MAPPING_FILE_PATH, "r", encoding="utf-8") as f:
|
||||
plc_mapping = json.load(f)
|
||||
logger.info("[MAPPING] Successfully loaded plc_mapping.json settings.")
|
||||
else:
|
||||
logger.warning("[MAPPING] plc_mapping.json does not exist. Default placeholders will be used.")
|
||||
plc_mapping = {"m_area": {}, "d_area": {}}
|
||||
except Exception as e:
|
||||
logger.error(f"[MAPPING] Failed to load plc_mapping.json: {e}")
|
||||
plc_mapping = {"m_area": {}, "d_area": {}}
|
||||
|
||||
# 최초 1회 즉시 로드
|
||||
load_plc_mapping()
|
||||
|
||||
# 각 라인 프리픽스별 동적 모멘터리 리셋 대기 비트 집합 (라인 -> set)
|
||||
pending_momentary_resets = {}
|
||||
|
||||
|
||||
# --- 헬스체크 ---
|
||||
def handle_health_check(client, payload_str, topic):
|
||||
logger.info(f"[MQTT] Health Check Ping received (Topic: {topic}). Replying with Pong.")
|
||||
client.publish('/app/health_check/pong', payload_str)
|
||||
|
||||
# --- 3단계 핵심: 특수 이벤트 핸들러 (999/998/997) ---
|
||||
def handle_system_event(client, payload_str, topic):
|
||||
"""
|
||||
이벤트 수신 시 DB 초기화 및 상태 제어를 담당합니다.
|
||||
- 999 (BOOT / Full-Sync 시작): DB 초기화 준비
|
||||
- 998 (PLC_REBOOT 감지 / Modbus 실패 10회): DB의 기계 상태값을 전부 0으로 강제 리셋
|
||||
- 997 (Full-Sync 완료): 정상 대기 상태 전환
|
||||
"""
|
||||
logger.info(f"[MQTT EVENT] Event received: {payload_str} (Topic: {topic})")
|
||||
try:
|
||||
data = json.loads(payload_str)
|
||||
event_id = data.get("id")
|
||||
|
||||
match = re.search(r'/([^/]+)/event', topic)
|
||||
line_prefix = match.group(1) if match else "line1"
|
||||
|
||||
if event_id == 998:
|
||||
logger.warn(f"[MQTT EVENT] PLC_REBOOT(998) 감지! {line_prefix} 관련 DB 실시간 상태를 0으로 리셋합니다.")
|
||||
_reset_machine_status_db(line_prefix)
|
||||
elif event_id == 999:
|
||||
logger.info(f"[MQTT EVENT] 게이트웨이 BOOT(999) 감지. {line_prefix} 관련 DB 실시간 상태를 0으로 리셋하고 Full-Sync를 개시합니다.")
|
||||
_reset_machine_status_db(line_prefix)
|
||||
elif event_id == 997:
|
||||
logger.info(f"[MQTT EVENT] Full-Sync 완료(997). 정상 대기 상태 전환.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[MQTT EVENT] Failed to process event: {e}", exc_info=True)
|
||||
|
||||
# --- 3단계 핵심: M영역/D영역 변경 배치 수신 및 DB Upsert ---
|
||||
def handle_m_in_batch(client, payload_str, topic):
|
||||
"""
|
||||
ESP32 -> 서버 (/line1/m/in)
|
||||
배치 데이터 [[id, val], [id, val], ...] 수신 시 DB 업데이트
|
||||
"""
|
||||
global pending_momentary_resets
|
||||
logger.debug(f"[MQTT M_IN] Batch raw data received: {payload_str}")
|
||||
try:
|
||||
batch = json.loads(payload_str)
|
||||
if not isinstance(batch, list):
|
||||
logger.warning("[MQTT M_IN] Payload is not a list array.")
|
||||
return
|
||||
|
||||
match = re.search(r'/([^/]+)/m/in', topic)
|
||||
line_prefix = match.group(1) if match else "line1"
|
||||
equipment_id = _get_equipment_id_by_prefix(line_prefix)
|
||||
|
||||
_update_batch_values_in_db(equipment_id, "m_area", batch, client)
|
||||
|
||||
# [신규 요구사항] 다음 읽기 영역 데이터를 받은 직후, 대기 중인 모멘터리 비트들을 0으로 복귀
|
||||
if line_prefix in pending_momentary_resets and pending_momentary_resets[line_prefix]:
|
||||
reset_bits = list(pending_momentary_resets[line_prefix])
|
||||
off_batch = [[bit_id, 0] for bit_id in reset_bits]
|
||||
reset_payload = json.dumps(off_batch)
|
||||
|
||||
# 대기 목록 비우기
|
||||
pending_momentary_resets[line_prefix].clear()
|
||||
|
||||
# ESP32 쓰기 영역 토픽(/m/out)으로 리셋 패킷 송신
|
||||
out_topic = f"/{line_prefix}/m/out"
|
||||
client.publish(out_topic, reset_payload)
|
||||
logger.info(f"[MQTT MOMENTARY DYNAMIC RESET] Sent reset command for bits {reset_bits} to {out_topic} after M_IN received.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[MQTT M_IN] Error processing batch: {e}", exc_info=True)
|
||||
|
||||
def handle_d_in_batch(client, payload_str, topic):
|
||||
"""
|
||||
ESP32 -> 서버 (/line1/d/in)
|
||||
배치 데이터 [[id, val], [id, val], ...] 수신 시 DB 업데이트
|
||||
"""
|
||||
logger.debug(f"[MQTT D_IN] Batch raw data received: {payload_str}")
|
||||
try:
|
||||
batch = json.loads(payload_str)
|
||||
if not isinstance(batch, list):
|
||||
logger.warning("[MQTT D_IN] Payload is not a list array.")
|
||||
return
|
||||
|
||||
match = re.search(r'/([^/]+)/d/in', topic)
|
||||
line_prefix = match.group(1) if match else "line1"
|
||||
equipment_id = _get_equipment_id_by_prefix(line_prefix)
|
||||
|
||||
_update_batch_values_in_db(equipment_id, "d_area", batch, client)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[MQTT D_IN] Error processing batch: {e}", exc_info=True)
|
||||
|
||||
|
||||
|
||||
# --- 3단계 핵심: 모멘터리(Momentary) 제어 로직 ---
|
||||
def handle_momentary_control(client, payload_str, topic):
|
||||
"""
|
||||
서버 -> ESP32 방향 (/line1/m/out)으로 제어 명령 전송 시 동작
|
||||
모멘터리 제어: 비트 ON 제어 명령을 대기열에 등록하고, 다음 M_IN(읽기 영역 수신) 직후 0으로 복귀시킵니다.
|
||||
"""
|
||||
global pending_momentary_resets
|
||||
logger.info(f"[MQTT MOMENTARY] Control command detected: {payload_str} (Topic: {topic})")
|
||||
try:
|
||||
batch = json.loads(payload_str)
|
||||
if not isinstance(batch, list):
|
||||
return
|
||||
|
||||
match = re.search(r'/([^/]+)/m/out', topic)
|
||||
line_prefix = match.group(1) if match else "line1"
|
||||
|
||||
# 1(ON)로 쓴 비트들만 추출하여 대기열에 추가
|
||||
on_bits = [item[0] for item in batch if len(item) >= 2 and item[1] == 1]
|
||||
|
||||
if on_bits:
|
||||
if line_prefix not in pending_momentary_resets:
|
||||
pending_momentary_resets[line_prefix] = set()
|
||||
for bit_id in on_bits:
|
||||
pending_momentary_resets[line_prefix].add(bit_id)
|
||||
logger.info(f"[MQTT MOMENTARY] Registered bits {on_bits} for line {line_prefix} for dynamic reset on next M_IN packet.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[MQTT MOMENTARY] Error handling momentary control: {e}")
|
||||
|
||||
|
||||
# --- DB 제어 관련 내부 헬퍼 함수 ---
|
||||
|
||||
def _get_equipment_id_by_prefix(prefix: str) -> int:
|
||||
"""
|
||||
토픽의 라인 구분자(prefix)를 기준으로 실제 DB의 설비 ID(PK)를 반환합니다.
|
||||
- line1 (ESP32 Gateway 토픽) -> 실제 DB의 양산 1호기 id = 8번에 대응
|
||||
"""
|
||||
if "line1" in prefix:
|
||||
return 8
|
||||
match = re.search(r'\d+', prefix)
|
||||
return int(match.group()) if match else 8
|
||||
|
||||
|
||||
def _reset_machine_status_db(line_prefix: str):
|
||||
"""PLC 연결 통신 장애 감지(998) 시 관련 장비 상태 데이터를 0 및 OFFLINE으로 강제 초기화"""
|
||||
equipment_id = _get_equipment_id_by_prefix(line_prefix)
|
||||
|
||||
# 208개 M_IN 비트와 188개 M_OUT 비트를 명시적으로 0으로 채운 JSON 문자열 생성
|
||||
initial_m_read = json.dumps({str(i): 0 for i in range(208)})
|
||||
initial_m_write = json.dumps({str(i): 0 for i in range(188)})
|
||||
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
sql = """
|
||||
UPDATE machine_status
|
||||
SET device_state = 'OFFLINE',
|
||||
m_read_raw = %s,
|
||||
m_write_raw = %s,
|
||||
d_read_g0 = 0, d_read_g1 = 0, d_read_g2 = 0, d_read_g3 = 0, d_read_g4 = 0,
|
||||
d_read_g5 = 0, d_read_g6 = 0, d_read_g7 = 0, d_read_g8 = 0, d_read_g9 = 0,
|
||||
timestamp = 0
|
||||
WHERE equipment_id = %s
|
||||
"""
|
||||
cursor.execute(sql, (initial_m_read, initial_m_write, equipment_id))
|
||||
conn.commit()
|
||||
logger.info(f"[DB RESET] Successfully reset machine_status columns (explicitly set all M bits to 0, excluded D-write columns) for equipment_id: {equipment_id}")
|
||||
except mysql.connector.Error as err:
|
||||
logger.error(f"[DB RESET] Failed to reset db: {err}")
|
||||
|
||||
def _update_batch_values_in_db(equipment_id: int, area_type: str, batch: list, client):
|
||||
"""
|
||||
ESP32로부터 수신된 변경 배치 데이터를 DB에 누적 병합(Merge)합니다.
|
||||
- M영역: m_read_raw JSON에 비트상태 누적 업데이트
|
||||
- D영역: 10개 d_read_g{0~9} 개별 컬럼에 수치값 매핑 업데이트
|
||||
"""
|
||||
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
cursor.execute("SELECT * FROM machine_status WHERE equipment_id = %s", (equipment_id,))
|
||||
status = cursor.fetchone()
|
||||
|
||||
if not status:
|
||||
cursor.execute(
|
||||
"INSERT INTO machine_status (equipment_id, device_state, m_read_raw, m_write_raw) VALUES (%s, 'ONLINE', '{}', '{}')",
|
||||
(equipment_id,)
|
||||
)
|
||||
conn.commit()
|
||||
cursor.execute("SELECT * FROM machine_status WHERE equipment_id = %s", (equipment_id,))
|
||||
status = cursor.fetchone()
|
||||
|
||||
# 기존 JSON 데이터 로드
|
||||
m_read = json.loads(status.get("m_read_raw") or '{}')
|
||||
m_write = json.loads(status.get("m_write_raw") or '{}')
|
||||
|
||||
updated = False
|
||||
update_fields = {}
|
||||
|
||||
if area_type == "m_area":
|
||||
for item in batch:
|
||||
if len(item) < 2:
|
||||
continue
|
||||
bit_id = str(item[0])
|
||||
val = item[1]
|
||||
m_read[bit_id] = val
|
||||
update_fields["m_read_raw"] = json.dumps(m_read)
|
||||
updated = True
|
||||
|
||||
elif area_type == "d_area":
|
||||
for item in batch:
|
||||
if len(item) < 2:
|
||||
continue
|
||||
group_id = int(item[0])
|
||||
val = int(item[1]) if group_id in (0, 5) else float(item[1])
|
||||
|
||||
if 0 <= group_id <= 9:
|
||||
# d_read_g0 ~ d_read_g9 컬럼 바인딩
|
||||
update_fields[f"d_read_g{group_id}"] = val
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
import time
|
||||
current_ts = int(time.time() * 1000)
|
||||
update_fields["timestamp"] = current_ts
|
||||
update_fields["device_state"] = 'ONLINE'
|
||||
|
||||
# 동적 SQL 쿼리 구성
|
||||
set_clause = ", ".join([f"{k} = %s" for k in update_fields.keys()])
|
||||
sql_update = f"UPDATE machine_status SET {set_clause} WHERE equipment_id = %s"
|
||||
|
||||
params = list(update_fields.values()) + [equipment_id]
|
||||
cursor.execute(sql_update, params)
|
||||
conn.commit()
|
||||
logger.debug(f"[DB UPDATE] Successfully updated machine_status for machine {equipment_id}")
|
||||
|
||||
_publish_hmi_update(equipment_id, client)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[DB BATCH] Error merging & updating batch values: {e}", exc_info=True)
|
||||
|
||||
def _get_prefix_by_equipment_id(equipment_id: int) -> str:
|
||||
"""DB 설비 ID에 대응하는 게이트웨이 토픽 프리픽스 조회"""
|
||||
if equipment_id == 8:
|
||||
return "line1"
|
||||
return f"line{equipment_id}"
|
||||
|
||||
def _publish_hmi_update(equipment_id: int, client):
|
||||
try:
|
||||
from equipment.db_manager import get_machine_status_detail_joined
|
||||
result = get_machine_status_detail_joined(equipment_id)
|
||||
if result:
|
||||
# Decimal 등의 변환 이슈 방지 (C-2)
|
||||
for i in range(10):
|
||||
key = f"d_read_g{i}"
|
||||
if key in result and result[key] is not None:
|
||||
try:
|
||||
result[key] = int(result[key])
|
||||
except Exception:
|
||||
pass
|
||||
for i in range(4):
|
||||
key = f"d_write_g{i}"
|
||||
if key in result and result[key] is not None:
|
||||
try:
|
||||
result[key] = int(result[key])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. --- 웹소켓 실시간 브로드캐스트 전송 (스레드 세이프 호출) ---
|
||||
import asyncio
|
||||
from communication.websocket_manager import ws_manager
|
||||
try:
|
||||
loop = getattr(ws_manager, 'loop', None)
|
||||
if loop and loop.is_running():
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
ws_manager.broadcast({
|
||||
"type": "equipment_update",
|
||||
"equipment_id": equipment_id,
|
||||
"data": result
|
||||
}),
|
||||
loop
|
||||
)
|
||||
except Exception as wse:
|
||||
logger.warning(f"[WS BROADCAST] Could not schedule websocket broadcast: {wse}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish HMI update: {e}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user