초기화
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# communication package marker
|
||||
from .mqtt_client import start_mqtt_client, stop_mqtt_client
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"lines": [
|
||||
{
|
||||
"line_id": "line1",
|
||||
"equipment_id": 8,
|
||||
"mqtt_prefix": "line1",
|
||||
"plc_slave_id": 1,
|
||||
"description": "양산 1호기 (CC_MC_TYPE_A)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, List
|
||||
|
||||
logger = logging.getLogger("line_manager")
|
||||
|
||||
class LineManager:
|
||||
def __init__(self, config_file: str):
|
||||
self.lines: Dict[str, dict] = {}
|
||||
self.equipment_to_prefix: Dict[int, str] = {}
|
||||
self.load_config(config_file)
|
||||
|
||||
def load_config(self, config_file: str):
|
||||
try:
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
for line_cfg in config.get('lines', []):
|
||||
line_id = line_cfg.get('line_id')
|
||||
equipment_id = line_cfg.get('equipment_id')
|
||||
|
||||
self.lines[line_id] = line_cfg
|
||||
self.equipment_to_prefix[equipment_id] = line_id
|
||||
|
||||
logger.info(f"[LINE CONFIG] Loaded {len(self.lines)} line configurations")
|
||||
else:
|
||||
logger.warning(f"[LINE CONFIG] Config file not found: {config_file}. Defaults to line1 -> ID 8.")
|
||||
except Exception as e:
|
||||
logger.error(f"[LINE CONFIG ERROR] Failed to load config: {e}")
|
||||
|
||||
def get_equipment_id(self, prefix: str) -> int:
|
||||
if prefix in self.lines:
|
||||
return self.lines[prefix].get('equipment_id', 8)
|
||||
# Default fallback
|
||||
if "line1" in prefix:
|
||||
return 8
|
||||
match = re.search(r'\d+', prefix)
|
||||
if match:
|
||||
line_num = int(match.group())
|
||||
return 7 + line_num
|
||||
return 8
|
||||
|
||||
def get_prefix(self, equipment_id: int) -> str:
|
||||
return self.equipment_to_prefix.get(equipment_id, "line1")
|
||||
|
||||
def get_all_prefixes(self) -> List[str]:
|
||||
if not self.lines:
|
||||
return ["line1"]
|
||||
return list(self.lines.keys())
|
||||
|
||||
# 전역 인스턴스
|
||||
config_path = os.path.join(os.path.dirname(__file__), "line_config.json")
|
||||
line_manager = LineManager(config_path)
|
||||
@@ -0,0 +1,321 @@
|
||||
import paho.mqtt.client as mqtt
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
import asyncio
|
||||
|
||||
logger = logging.getLogger("mqtt_client")
|
||||
|
||||
# ── 설정 ──────────────────────────────────────────────────
|
||||
BROKER_HOST = "localhost"
|
||||
BROKER_PORT = 1883
|
||||
|
||||
# MQTT 토픽 규칙:
|
||||
# 장비→서버(읽기): /{prefix}/m/in, /{prefix}/d/in, /{prefix}/event
|
||||
# 서버→장비(쓰기): /{prefix}/m/out, /{prefix}/d/out
|
||||
|
||||
_client: mqtt.Client = None
|
||||
|
||||
|
||||
from .line_manager import line_manager
|
||||
|
||||
def _get_equipment_id_by_prefix(prefix: str) -> int:
|
||||
"""
|
||||
토픽 프리픽스를 기반으로 DB의 설비 ID를 반환합니다 (LineManager 연동).
|
||||
"""
|
||||
return line_manager.get_equipment_id(prefix)
|
||||
|
||||
|
||||
def _publish_hmi_update(equipment_id: int):
|
||||
"""
|
||||
HMI 실시간 상태 업데이트 데이터를 조회하여 WebSocket으로 브로드캐스트합니다.
|
||||
MQTT 수신 스레드가 블로킹되는 현상을 피하기 위해 Future 대기 없이 이벤트 루프에 비동기 처리만 위임합니다.
|
||||
"""
|
||||
try:
|
||||
from equipment.db_manager import get_machine_status_detail_joined
|
||||
from communication.websocket_manager import ws_manager
|
||||
|
||||
result = get_machine_status_detail_joined(equipment_id)
|
||||
if result:
|
||||
# Decimal 타입 등의 변환 오류 방지 안전 필터 적용
|
||||
# 그룹 0,5(순환펌프 재작동 시간)만 정수, 나머지 d_read와 모든 d_write는 PLC REAL이므로 소수점 보존
|
||||
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]) if i in (0, 5) else float(result[key])
|
||||
except:
|
||||
result[key] = 0
|
||||
for i in range(4):
|
||||
key = f"d_write_g{i}"
|
||||
if key in result and result[key] is not None:
|
||||
try:
|
||||
result[key] = float(result[key])
|
||||
except:
|
||||
result[key] = 0
|
||||
|
||||
# 웹소켓 매니저의 메인 이벤트 루프로 비동기 브로드캐스트 위임
|
||||
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,
|
||||
"timestamp": int(time.time() * 1000)
|
||||
}),
|
||||
loop
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[WS BROADCAST ERROR] Failed to broadcast for equipment_id {equipment_id}: {e}", exc_info=True)
|
||||
|
||||
|
||||
def _on_connect(client, userdata, flags, rc):
|
||||
if rc == 0:
|
||||
logger.info("[MQTT] Connected to broker successfully.")
|
||||
|
||||
# line_config.json에 등록된 모든 라인의 토픽 동적 구독
|
||||
prefixes = line_manager.get_all_prefixes()
|
||||
for prefix in prefixes:
|
||||
client.subscribe(f"/{prefix}/m/in")
|
||||
client.subscribe(f"/{prefix}/d/in")
|
||||
client.subscribe(f"/{prefix}/event")
|
||||
client.subscribe(f"/{prefix}/ack")
|
||||
client.subscribe(f"/{prefix}/status")
|
||||
|
||||
logger.info(f"[MQTT] Dynamically subscribed to topics for prefixes: {prefixes}")
|
||||
else:
|
||||
logger.error(f"[MQTT] Connection failed: rc={rc}")
|
||||
|
||||
|
||||
def _on_message(client, userdata, msg):
|
||||
"""
|
||||
배치 데이터 [[id, val], ...] 수집 및 DB 동기화, HMI 실시간 전계 (성능 모니터링 포함)
|
||||
"""
|
||||
start_time = time.time()
|
||||
topic = msg.topic
|
||||
try:
|
||||
payload_str = msg.payload.decode()
|
||||
logger.info(f"[MQTT RECEIVE] Topic: {topic} | Payload: {payload_str}")
|
||||
|
||||
payload = json.loads(payload_str)
|
||||
|
||||
# 토픽 프리픽스 동적 해석
|
||||
match = re.search(r'/([^/]+)/(m|d|event|ack|status)', topic)
|
||||
line_prefix = match.group(1) if match else "line1"
|
||||
equipment_id = _get_equipment_id_by_prefix(line_prefix)
|
||||
|
||||
from database.connection import get_db_connection
|
||||
|
||||
if "/ack" in topic:
|
||||
command_id = payload.get("command_id")
|
||||
status = payload.get("status")
|
||||
applied_value = payload.get("applied_value") # 적용 데이터 파싱
|
||||
logger.info(f"[MQTT ACK Callback] Received Event command {command_id} status={status} applied_value={applied_value} for prefix: {line_prefix}")
|
||||
|
||||
# DB 상태 및 적용값 분리 저장 업데이트
|
||||
from equipment.db_manager import update_control_command_status
|
||||
update_control_command_status(command_id, status, applied_value)
|
||||
|
||||
# WebSocket HMI 브로드캐스트
|
||||
from communication.websocket_manager import ws_manager
|
||||
loop = getattr(ws_manager, 'loop', None)
|
||||
if loop and loop.is_running():
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
ws_manager.broadcast({
|
||||
"type": "command_ack",
|
||||
"command_id": command_id,
|
||||
"status": status,
|
||||
"applied_value": applied_value,
|
||||
"timestamp": int(time.time() * 1000)
|
||||
}),
|
||||
loop
|
||||
)
|
||||
|
||||
elif "/status" in topic:
|
||||
gateway = payload.get("gateway", "OFFLINE")
|
||||
plc_state = payload.get("plc_state", "OFFLINE")
|
||||
|
||||
logger.info(
|
||||
f"[MQTT STATUS Callback] gateway={gateway}, "
|
||||
f"plc_state={plc_state} for prefix: {line_prefix}"
|
||||
)
|
||||
|
||||
# 상태 토픽을 기준으로 장비 상태와 동기화 상태를 함께 갱신
|
||||
sync_state = None
|
||||
|
||||
if gateway == "OFFLINE" or plc_state == "OFFLINE":
|
||||
device_state = "OFFLINE"
|
||||
elif gateway == "SYNCING":
|
||||
device_state = "SYNCING"
|
||||
sync_state = "SYNCING"
|
||||
else:
|
||||
device_state = "ONLINE"
|
||||
sync_state = "IDLE"
|
||||
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE machine_status
|
||||
SET gateway_state = %s,
|
||||
plc_state = %s,
|
||||
device_state = %s,
|
||||
sync_state = COALESCE(%s, sync_state),
|
||||
timestamp = CURRENT_TIMESTAMP
|
||||
WHERE equipment_id = %s
|
||||
""",
|
||||
(
|
||||
gateway,
|
||||
plc_state,
|
||||
device_state,
|
||||
sync_state,
|
||||
equipment_id
|
||||
)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
_publish_hmi_update(equipment_id)
|
||||
|
||||
elif "/event" in topic:
|
||||
event_id = payload.get("id")
|
||||
event_val = payload.get("val")
|
||||
logger.info(f"[MQTT Event Callback] Received Event {event_id} (value={event_val}) for prefix: {line_prefix}")
|
||||
|
||||
# Full-Sync 혹은 PLC 오프라인 상태값 업데이트
|
||||
device_state = "ONLINE"
|
||||
sync_state = "IDLE"
|
||||
if event_id == 999: # Full-Sync 시작 (STATE_INIT 진입)
|
||||
device_state = "SYNCING"
|
||||
sync_state = "SYNCING"
|
||||
elif event_id == 998: # PLC Modbus 통신 실패 감지
|
||||
device_state = "OFFLINE"
|
||||
elif event_id == 997: # Full-Sync 완료 (정상 루프 진입)
|
||||
device_state = "ONLINE"
|
||||
sync_state = "IDLE"
|
||||
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
if event_id == 998:
|
||||
cursor.execute(
|
||||
"UPDATE machine_status SET device_state = %s, plc_state = 'OFFLINE', timestamp = CURRENT_TIMESTAMP WHERE equipment_id = %s",
|
||||
(device_state, equipment_id)
|
||||
)
|
||||
else:
|
||||
cursor.execute(
|
||||
"UPDATE machine_status SET device_state = %s, sync_state = %s, timestamp = CURRENT_TIMESTAMP WHERE equipment_id = %s",
|
||||
(device_state, sync_state, equipment_id)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"[MQTT Event DB Update Completed] device_state updated to: {device_state}")
|
||||
_publish_hmi_update(equipment_id)
|
||||
|
||||
elif "/m/in" in topic:
|
||||
if not isinstance(payload, list):
|
||||
raise ValueError("M_in payload is not a JSON list")
|
||||
logger.info(f"[MQTT Processing M_in] Size of incoming batch: {len(payload)} for prefix: {line_prefix}")
|
||||
# 1. 기존 m_read_raw 및 m_write_raw JSON 데이터 조회
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
cursor.execute("SELECT m_read_raw, m_write_raw FROM machine_status WHERE equipment_id = %s", (equipment_id,))
|
||||
row = cursor.fetchone()
|
||||
|
||||
m_read = {}
|
||||
if row and row.get("m_read_raw"):
|
||||
try:
|
||||
m_read = json.loads(row["m_read_raw"])
|
||||
except:
|
||||
pass
|
||||
|
||||
m_write = {}
|
||||
if row and row.get("m_write_raw"):
|
||||
try:
|
||||
m_write = json.loads(row["m_write_raw"])
|
||||
except:
|
||||
pass
|
||||
|
||||
# 2. 배치 배열 수신값 머지 [[bit_id, val], ...]
|
||||
for item in payload:
|
||||
if isinstance(item, list) and len(item) == 2:
|
||||
bit_id = str(item[0])
|
||||
val = int(item[1])
|
||||
m_read[bit_id] = val
|
||||
|
||||
# 3. 모멘터리 비트 정리: 쓰기 버퍼(m_write_raw) 중 1로 켜져있던 비트들을 0으로 복구
|
||||
# (ESP32 측에서 이미 쓰기 실행 및 자동 리셋을 처리했으므로 서버측 DB만 동기화하고 패킷은 송신 안함)
|
||||
for k, v in m_write.items():
|
||||
if v == 1:
|
||||
m_write[k] = 0
|
||||
|
||||
# 4. DB에 병합된 M 데이터 업데이트
|
||||
cursor.execute(
|
||||
"UPDATE machine_status SET m_read_raw = %s, m_write_raw = %s, device_state = 'ONLINE', timestamp = CURRENT_TIMESTAMP WHERE equipment_id = %s",
|
||||
(json.dumps(m_read), json.dumps(m_write), equipment_id)
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"[MQTT Processing M_in Completed] Successfully merged and saved {len(payload)} bits to DB.")
|
||||
_publish_hmi_update(equipment_id)
|
||||
|
||||
elif "/d/in" in topic:
|
||||
if not isinstance(payload, list):
|
||||
raise ValueError("D_in payload is not a JSON list")
|
||||
logger.info(f"[MQTT Processing D_in] Size of incoming batch: {len(payload)} for prefix: {line_prefix}")
|
||||
# D영역 업데이트: [[group_id, val], ...]
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
for item in payload:
|
||||
if isinstance(item, list) and len(item) == 2:
|
||||
group_id = int(item[0])
|
||||
# 그룹 0,5(순환펌프 재작동 시간)는 UINT16 정수, 그 외는 PLC REAL이므로 소수점 보존
|
||||
val = int(item[1]) if group_id in (0, 5) else float(item[1])
|
||||
if 0 <= group_id <= 9:
|
||||
sql = f"UPDATE machine_status SET d_read_g{group_id} = %s, device_state = 'ONLINE', timestamp = CURRENT_TIMESTAMP WHERE equipment_id = %s"
|
||||
cursor.execute(sql, (val, equipment_id))
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"[MQTT Processing D_in Completed] Successfully saved {len(payload)} D-registers to DB.")
|
||||
_publish_hmi_update(equipment_id)
|
||||
|
||||
# 성공 처리 시간 기록
|
||||
duration_ms = (time.time() - start_time) * 1000
|
||||
from monitoring.metrics import record_metric
|
||||
record_metric(topic, duration_ms)
|
||||
|
||||
except Exception as e:
|
||||
duration_ms = (time.time() - start_time) * 1000
|
||||
from monitoring.metrics import record_metric
|
||||
record_metric(topic, duration_ms, error=True, error_msg=str(e))
|
||||
logger.error(f"[MQTT ERROR] Message parse/update error: {e}", exc_info=True)
|
||||
|
||||
|
||||
def start_mqtt_client(db=None):
|
||||
"""
|
||||
FastAPI 어플리케이션 구동 시 호출됩니다.
|
||||
"""
|
||||
global _client
|
||||
_client = mqtt.Client(client_id="ChemiFactory_HMI_Server")
|
||||
|
||||
# MQTT 인증 계정 정보 적용 (rc=5 방지)
|
||||
_client.username_pw_set("ctnt_root", "Umsang6595!!")
|
||||
|
||||
_client.on_connect = _on_connect
|
||||
_client.on_message = _on_message
|
||||
|
||||
try:
|
||||
logger.info(f"[MQTT INIT] Connecting to broker at {BROKER_HOST}:{BROKER_PORT}...")
|
||||
_client.connect(BROKER_HOST, BROKER_PORT, 60)
|
||||
_client.loop_start()
|
||||
logger.info("[MQTT INIT] Background client started and loop active.")
|
||||
except Exception as e:
|
||||
logger.error(f"[MQTT INIT ERROR] Startup failed: {e}", exc_info=True)
|
||||
|
||||
|
||||
def stop_mqtt_client():
|
||||
global _client
|
||||
if _client:
|
||||
_client.loop_stop()
|
||||
_client.disconnect()
|
||||
logger.info("[MQTT] Background client stopped successfully.")
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
{
|
||||
"comment": "이 파일은 PLC의 읽기(입력) 영역과 쓰기(제어) 영역의 센서/동작명을 정의합니다.",
|
||||
|
||||
"m_read_area": {
|
||||
"0": "A그룹 | 인터락 리셋 상태",
|
||||
"1": "A그룹 | E/STOP 상태",
|
||||
"2": "A그룹 | DOOR좌 인터락 상태",
|
||||
"3": "A그룹 | DOOR우 인터락 상태",
|
||||
"4": "A그룹 | 수위 인터락 상태",
|
||||
"5": "A그룹 | 히터 인터락 상태",
|
||||
"6": "A그룹 | 전체그룹 활성화 ON 상태",
|
||||
"7": "A그룹 | 전체그룹 활성화 OFF 상태(반전신호)",
|
||||
"8": "A그룹 | 전체그룹 정지 상태",
|
||||
"9": "A그룹 | 전체그룹 일시정지 상태",
|
||||
"10": "A그룹 | 전체그룹 재시작 상태",
|
||||
"11": "A그룹 | 함침,이송 그룹 ON 상태",
|
||||
"12": "A그룹 | 함침,이송 그룹 OFF 상태(반전신호)",
|
||||
"13": "A그룹 | 건조부 그룹 ON 상태",
|
||||
"14": "A그룹 | 건조부 그룹 OFF 상태(반전신호)",
|
||||
"15": "A그룹 | 커팅부 그룹 ON 상태",
|
||||
"16": "A그룹 | 커팅부 그룹 OFF 상태(반전신호)",
|
||||
"17": "A그룹 | 이송부 그룹 ON 상태",
|
||||
"18": "A그룹 | 이송부 그룹 OFF 상태(반전신호)",
|
||||
"19": "B그룹 | 인터락 리셋 상태",
|
||||
"20": "B그룹 | E/STOP 상태",
|
||||
"21": "B그룹 | DOOR좌 인터락 상태",
|
||||
"22": "B그룹 | DOOR우 인터락 상태",
|
||||
"23": "B그룹 | 수위 인터락 상태",
|
||||
"24": "B그룹 | 히터 인터락 상태",
|
||||
"25": "B그룹 | 전체그룹 활성화 ON 상태",
|
||||
"26": "B그룹 | 전체그룹 활성화 OFF 상태(반전신호)",
|
||||
"27": "B그룹 | 전체그룹 정지 상태",
|
||||
"28": "B그룹 | 전체그룹 일시정지 상태",
|
||||
"29": "B그룹 | 전체그룹 재시작 상태",
|
||||
"30": "B그룹 | 함침,이송 그룹 ON 상태",
|
||||
"31": "B그룹 | 함침,이송 그룹 OFF 상태(반전신호)",
|
||||
"32": "B그룹 | 건조부 그룹 ON 상태",
|
||||
"33": "B그룹 | 건조부 그룹 OFF 상태(반전신호)",
|
||||
"34": "B그룹 | 커팅부 그룹 ON 상태",
|
||||
"35": "B그룹 | 커팅부 그룹 OFF 상태(반전신호)",
|
||||
"36": "B그룹 | 이송부 그룹 ON 상태",
|
||||
"37": "B그룹 | 이송부 그룹 OFF 상태(반전신호)",
|
||||
"38": "A그룹 | 함침부 자동모드 ON 상태",
|
||||
"39": "A그룹 | 함침부 수동모드 ON 상태",
|
||||
"40": "A그룹 | 순환 펌프 상태",
|
||||
"41": "A그룹 | 배출 펌프 상태",
|
||||
"42": "B그룹 | 함침부 자동모드 ON 상태",
|
||||
"43": "B그룹 | 함침부 수동모드 ON 상태",
|
||||
"44": "B그룹 | 순환 펌프 상태",
|
||||
"45": "B그룹 | 배출 펌프 상태",
|
||||
"46": "A그룹 | 건조부 자동모드 ON 상태",
|
||||
"47": "A그룹 | 건조부 수동모드 ON 상태",
|
||||
"48": "A그룹 | 송풍기 트립 상태",
|
||||
"49": "A그룹 | 히터 제어요청 상태",
|
||||
"50": "A그룹 | 송풍기1 작동 상태",
|
||||
"51": "A그룹 | 히터 1 작동 상태",
|
||||
"52": "A그룹 | 히터 2 작동 상태",
|
||||
"53": "A그룹 | 히터 3 작동 상태",
|
||||
"54": "A그룹 | 히터 4 작동 상태",
|
||||
"55": "A그룹 | 히터 5 작동 상태",
|
||||
"56": "B그룹 | 건조부 자동모드 ON 상태",
|
||||
"57": "B그룹 | 건조부 수동모드 ON 상태",
|
||||
"58": "B그룹 | 송풍기2 트립 상태",
|
||||
"59": "B그룹 | 히터 제어요청 상태",
|
||||
"60": "B그룹 | 송풍기2 작동 상태",
|
||||
"61": "B그룹 | 히터 6 작동 상태",
|
||||
"62": "B그룹 | 히터 7 작동 상태",
|
||||
"63": "B그룹 | 히터 8 작동 상태",
|
||||
"64": "B그룹 | 히터 9 작동 상태",
|
||||
"65": "B그룹 | 히터 10 작동 상태",
|
||||
"66": "A그룹 | 커팅모터 활성 상태",
|
||||
"67": "A그룹 | 커팅모터 시작 상태",
|
||||
"68": "B그룹 | 커팅모터 활성 상태",
|
||||
"69": "B그룹 | 커팅모터 시작 상태",
|
||||
"70": "A그룹 | 이송부 자동모드 ON 상태",
|
||||
"71": "A그룹 | 이송부 수동모드 ON 상태",
|
||||
"72": "A그룹 | 이송모터 자동시작 상태",
|
||||
"73": "A그룹 | 이송모터 자동정지 상태",
|
||||
"74": "A그룹 | 이송모터 수동 정방향 시작 상태",
|
||||
"75": "A그룹 | 이송모터 수동 역방향 시작 상태",
|
||||
"76": "A그룹 | 이송모터 수동 정지 상태",
|
||||
"77": "A그룹 | 이송모터 제로셋 상태",
|
||||
"78": "A그룹 | 이송모터 에러리셋 상태",
|
||||
"79": "B그룹 | 이송부 자동모드 ON 상태",
|
||||
"80": "B그룹 | 이송부 수동모드 ON 상태",
|
||||
"81": "B그룹 | 이송모터 자동시작 상태",
|
||||
"82": "B그룹 | 이송모터 자동정지 상태",
|
||||
"83": "B그룹 | 이송모터 수동 정방향 시작 상태",
|
||||
"84": "B그룹 | 이송모터 수동 역방향 시작 상태",
|
||||
"85": "B그룹 | 이송모터 수동 정지 상태",
|
||||
"86": "B그룹 | 이송모터 제로셋 상태",
|
||||
"87": "B그룹 | 이송모터 에러리셋 상태",
|
||||
"88": "A그룹 | 포트01 자동하강 상태",
|
||||
"89": "A그룹 | 포트02 자동하강 상태",
|
||||
"90": "A그룹 | 포트03 자동하강 상태",
|
||||
"91": "A그룹 | 포트04 자동하강 상태",
|
||||
"92": "A그룹 | 포트05 자동하강 상태",
|
||||
"93": "A그룹 | 포트06 자동하강 상태",
|
||||
"94": "A그룹 | 포트07 자동하강 상태",
|
||||
"95": "A그룹 | 포트08 자동하강 상태",
|
||||
"96": "A그룹 | 포트09 자동하강 상태",
|
||||
"97": "A그룹 | 포트10 자동하강 상태",
|
||||
"98": "A그룹 | 포트11 자동하강 상태",
|
||||
"99": "A그룹 | 포트12 자동하강 상태",
|
||||
"100": "A그룹 | 포트13 자동하강 상태",
|
||||
"101": "A그룹 | 포트14 자동하강 상태",
|
||||
"102": "A그룹 | 포트15 자동하강 상태",
|
||||
"103": "A그룹 | 포트16 자동하강 상태",
|
||||
"104": "A그룹 | 포트17 자동하강 상태",
|
||||
"105": "A그룹 | 포트18 자동하강 상태",
|
||||
"106": "A그룹 | 포트19 자동하강 상태",
|
||||
"107": "A그룹 | 포트20 자동하강 상태",
|
||||
"108": "B그룹 | 포트21 자동하강 상태",
|
||||
"109": "B그룹 | 포트22 자동하강 상태",
|
||||
"110": "B그룹 | 포트23 자동하강 상태",
|
||||
"111": "B그룹 | 포트24 자동하강 상태",
|
||||
"112": "B그룹 | 포트25 자동하강 상태",
|
||||
"113": "B그룹 | 포트26 자동하강 상태",
|
||||
"114": "B그룹 | 포트27 자동하강 상태",
|
||||
"115": "B그룹 | 포트28 자동하강 상태",
|
||||
"116": "B그룹 | 포트29 자동하강 상태",
|
||||
"117": "B그룹 | 포트30 자동하강 상태",
|
||||
"118": "B그룹 | 포트31 자동하강 상태",
|
||||
"119": "B그룹 | 포트32 자동하강 상태",
|
||||
"120": "B그룹 | 포트33 자동하강 상태",
|
||||
"121": "B그룹 | 포트34 자동하강 상태",
|
||||
"122": "B그룹 | 포트35 자동하강 상태",
|
||||
"123": "B그룹 | 포트36 자동하강 상태",
|
||||
"124": "B그룹 | 포트37 자동하강 상태",
|
||||
"125": "B그룹 | 포트38 자동하강 상태",
|
||||
"126": "B그룹 | 포트39 자동하강 상태",
|
||||
"127": "B그룹 | 포트40 자동하강 상태",
|
||||
"128": "A그룹 | 포트01 수동하강 상태",
|
||||
"129": "A그룹 | 포트02 수동하강 상태",
|
||||
"130": "A그룹 | 포트03 수동하강 상태",
|
||||
"131": "A그룹 | 포트04 수동하강 상태",
|
||||
"132": "A그룹 | 포트05 수동하강 상태",
|
||||
"133": "A그룹 | 포트06 수동하강 상태",
|
||||
"134": "A그룹 | 포트07 수동하강 상태",
|
||||
"135": "A그룹 | 포트08 수동하강 상태",
|
||||
"136": "A그룹 | 포트09 수동하강 상태",
|
||||
"137": "A그룹 | 포트10 수동하강 상태",
|
||||
"138": "A그룹 | 포트11 수동하강 상태",
|
||||
"139": "A그룹 | 포트12 수동하강 상태",
|
||||
"140": "A그룹 | 포트13 수동하강 상태",
|
||||
"141": "A그룹 | 포트14 수동하강 상태",
|
||||
"142": "A그룹 | 포트15 수동하강 상태",
|
||||
"143": "A그룹 | 포트16 수동하강 상태",
|
||||
"144": "A그룹 | 포트17 수동하강 상태",
|
||||
"145": "A그룹 | 포트18 수동하강 상태",
|
||||
"146": "A그룹 | 포트19 수동하강 상태",
|
||||
"147": "A그룹 | 포트20 수동하강 상태",
|
||||
"148": "B그룹 | 포트21 수동하강 상태",
|
||||
"149": "B그룹 | 포트22 수동하강 상태",
|
||||
"150": "B그룹 | 포트23 수동하강 상태",
|
||||
"151": "B그룹 | 포트24 수동하강 상태",
|
||||
"152": "B그룹 | 포트25 수동하강 상태",
|
||||
"153": "B그룹 | 포트26 수동하강 상태",
|
||||
"154": "B그룹 | 포트27 수동하강 상태",
|
||||
"155": "B그룹 | 포트28 수동하강 상태",
|
||||
"156": "B그룹 | 포트29 수동하강 상태",
|
||||
"157": "B그룹 | 포트30 수동하강 상태",
|
||||
"158": "B그룹 | 포트31 수동하강 상태",
|
||||
"159": "B그룹 | 포트32 수동하강 상태",
|
||||
"160": "B그룹 | 포트33 수동하강 상태",
|
||||
"161": "B그룹 | 포트34 수동하강 상태",
|
||||
"162": "B그룹 | 포트35 수동하강 상태",
|
||||
"163": "B그룹 | 포트36 수동하강 상태",
|
||||
"164": "B그룹 | 포트37 수동하강 상태",
|
||||
"165": "B그룹 | 포트38 수동하강 상태",
|
||||
"166": "B그룹 | 포트39 수동하강 상태",
|
||||
"167": "B그룹 | 포트40 수동하강 상태",
|
||||
"168": "A그룹 | 포트01 자동상승 상태",
|
||||
"169": "A그룹 | 포트02 수동상승 상태",
|
||||
"170": "A그룹 | 포트03 수동상승 상태",
|
||||
"171": "A그룹 | 포트04 수동상승 상태",
|
||||
"172": "A그룹 | 포트05 수동상승 상태",
|
||||
"173": "A그룹 | 포트06 수동상승 상태",
|
||||
"174": "A그룹 | 포트07 수동상승 상태",
|
||||
"175": "A그룹 | 포트08 수동상승 상태",
|
||||
"176": "A그룹 | 포트09 수동상승 상태",
|
||||
"177": "A그룹 | 포트10 수동상승 상태",
|
||||
"178": "A그룹 | 포트11 수동상승 상태",
|
||||
"179": "A그룹 | 포트12 수동상승 상태",
|
||||
"180": "A그룹 | 포트13 수동상승 상태",
|
||||
"181": "A그룹 | 포트14 수동상승 상태",
|
||||
"182": "A그룹 | 포트15 수동상승 상태",
|
||||
"183": "A그룹 | 포트16 수동상승 상태",
|
||||
"184": "A그룹 | 포트17 수동상승 상태",
|
||||
"185": "A그룹 | 포트18 수동상승 상태",
|
||||
"186": "A그룹 | 포트19 수동상승 상태",
|
||||
"187": "A그룹 | 포트20 수동상승 상태",
|
||||
"188": "B그룹 | 포트21 수동상승 상태",
|
||||
"189": "B그룹 | 포트22 수동상승 상태",
|
||||
"190": "B그룹 | 포트23 수동상승 상태",
|
||||
"191": "B그룹 | 포트24 수동상승 상태",
|
||||
"192": "B그룹 | 포트25 수동상승 상태",
|
||||
"193": "B그룹 | 포트26 수동상승 상태",
|
||||
"194": "B그룹 | 포트27 수동상승 상태",
|
||||
"195": "B그룹 | 포트28 수동상승 상태",
|
||||
"196": "B그룹 | 포트29 수동상승 상태",
|
||||
"197": "B그룹 | 포트30 수동상승 상태",
|
||||
"198": "B그룹 | 포트31 수동상승 상태",
|
||||
"199": "B그룹 | 포트32 수동상승 상태",
|
||||
"200": "B그룹 | 포트33 수동상승 상태",
|
||||
"201": "B그룹 | 포트34 수동상승 상태",
|
||||
"202": "B그룹 | 포트35 수동상승 상태",
|
||||
"203": "B그룹 | 포트36 수동상승 상태",
|
||||
"204": "B그룹 | 포트37 수동상승 상태",
|
||||
"205": "B그룹 | 포트38 수동상승 상태",
|
||||
"206": "B그룹 | 포트39 수동상승 상태",
|
||||
"207": "B그룹 | 포트40 수동상승 상태"
|
||||
},
|
||||
|
||||
"d_read_area": {
|
||||
"0": "A그룹 | 순환펌프 재작동 시간",
|
||||
"1": "A그룹 | 커팅모터 속도",
|
||||
"2": "A그룹 | 이송모터 자동 목표 이동량",
|
||||
"3": "A그룹 | 이송모터 1Hz 기준 현재 이동량",
|
||||
"4": "A그룹 | 이송모터 수동 목표 속도",
|
||||
"5": "B그룹 | 순환펌프 재작동 시간",
|
||||
"6": "B그룹 | 커팅모터 속도",
|
||||
"7": "B그룹 | 이송모터 자동 목표 이동량",
|
||||
"8": "B그룹 | 이송모터 1Hz 기준 현재 이동량",
|
||||
"9": "B그룹 | 이송모터 수동 목표 속도"
|
||||
},
|
||||
|
||||
"m_write_area": {
|
||||
"0": "A그룹 | 인터락 리셋 버튼",
|
||||
"1": "A그룹 | 전체그룹 활성화 ON 버튼",
|
||||
"2": "A그룹 | 전체그룹 활성화 OFF 버튼",
|
||||
"3": "A그룹 | 전체그룹 정지 버튼",
|
||||
"4": "A그룹 | 함침,이송 그룹 ON 버튼",
|
||||
"5": "A그룹 | 함침,이송 그룹 OFF 버튼",
|
||||
"6": "A그룹 | 건조부 그룹 ON 버튼",
|
||||
"7": "A그룹 | 건조부 그룹 OFF 버튼",
|
||||
"8": "A그룹 | 커팅부 그룹 ON 버튼",
|
||||
"9": "A그룹 | 커팅부 그룹 OFF 버튼",
|
||||
"10": "A그룹 | 이송부 그룹 ON 버튼",
|
||||
"11": "A그룹 | 이송부 그룹 OFF 버튼",
|
||||
"12": "B그룹 | 인터락 리셋 버튼",
|
||||
"13": "B그룹 | 전체그룹 활성화 ON 버튼",
|
||||
"14": "B그룹 | 전체그룹 활성화 OFF 버튼",
|
||||
"15": "B그룹 | 전체그룹 정지 버튼",
|
||||
"16": "B그룹 | 함침,이송 그룹 ON 버튼",
|
||||
"17": "B그룹 | 함침,이송 그룹 OFF 버튼",
|
||||
"18": "B그룹 | 건조부 그룹 ON 버튼",
|
||||
"19": "B그룹 | 건조부 그룹 OFF 버튼",
|
||||
"20": "B그룹 | 커팅부 그룹 ON 버튼",
|
||||
"21": "B그룹 | 커팅부 그룹 OFF 버튼",
|
||||
"22": "B그룹 | 이송부 그룹 ON 버튼",
|
||||
"23": "B그룹 | 이송부 그룹 OFF 버튼",
|
||||
"24": "A그룹 | 함침부 자동모드 ON 버튼",
|
||||
"25": "A그룹 | 함침부 수동모드 ON 버튼",
|
||||
"26": "A그룹 | 순환 펌프 작동 버튼",
|
||||
"27": "A그룹 | 배출 펌프 작동 버튼",
|
||||
"28": "B그룹 | 함침부 자동모드 ON 버튼",
|
||||
"29": "B그룹 | 함침부 수동모드 ON 버튼",
|
||||
"30": "B그룹 | 순환 펌프 작동 버튼",
|
||||
"31": "B그룹 | 배출 펌프 작동 버튼",
|
||||
"32": "A그룹 | 건조부 자동모드 ON 버튼",
|
||||
"33": "A그룹 | 건조부 수동모드 ON 버튼",
|
||||
"34": "A그룹 | 송풍기1 작동 버튼",
|
||||
"35": "A그룹 | 히터 1 작동 버튼",
|
||||
"36": "A그룹 | 히터 2 작동 버튼",
|
||||
"37": "A그룹 | 히터 3 작동 버튼",
|
||||
"38": "A그룹 | 히터 4 작동 버튼",
|
||||
"39": "A그룹 | 히터 5 작동 버튼",
|
||||
"40": "B그룹 | 건조부 자동모드 ON 버튼",
|
||||
"41": "B그룹 | 건조부 수동모드 ON 버튼",
|
||||
"42": "B그룹 | 송풍기2 작동 버튼",
|
||||
"43": "B그룹 | 히터 6 작동 버튼",
|
||||
"44": "B그룹 | 히터 7 작동 버튼",
|
||||
"45": "B그룹 | 히터 8 작동 버튼",
|
||||
"46": "B그룹 | 히터 9 작동 버튼",
|
||||
"47": "B그룹 | 히터 10 작동 버튼",
|
||||
"48": "A그룹 | 커팅모터 작동 버튼",
|
||||
"49": "B그룹 | 커팅모터 작동 버튼",
|
||||
"50": "A그룹 | 이송부 자동모드 ON 버튼",
|
||||
"51": "A그룹 | 이송부 수동모드 ON 버튼",
|
||||
"52": "A그룹 | 이송모터 자동시작 버튼",
|
||||
"53": "A그룹 | 이송모터 자동정지 버튼",
|
||||
"54": "A그룹 | 이송모터 수동 정방향 시작 버튼",
|
||||
"55": "A그룹 | 이송모터 수동 역방향 시작 버튼",
|
||||
"56": "A그룹 | 이송모터 수동 정지 버튼",
|
||||
"57": "A그룹 | 이송모터 제로셋 버튼",
|
||||
"58": "A그룹 | 이송모터 에러리셋 버튼",
|
||||
"59": "B그룹 | 이송부 자동모드 ON 버튼",
|
||||
"60": "B그룹 | 이송부 수동모드 ON 버튼",
|
||||
"61": "B그룹 | 이송모터 자동시작 버튼",
|
||||
"62": "B그룹 | 이송모터 자동정지 버튼",
|
||||
"63": "B그룹 | 이송모터 수동 정방향 시작 버튼",
|
||||
"64": "B그룹 | 이송모터 수동 역방향 시작 버튼",
|
||||
"65": "B그룹 | 이송모터 수동 정지 버튼",
|
||||
"66": "B그룹 | 이송모터 제로셋 버튼",
|
||||
"67": "B그룹 | 이송모터 에러리셋 버튼",
|
||||
"68": "A그룹 | 포트01 자동하강 버튼",
|
||||
"69": "A그룹 | 포트02 자동하강 버튼",
|
||||
"70": "A그룹 | 포트03 자동하강 버튼",
|
||||
"71": "A그룹 | 포트04 자동하강 버튼",
|
||||
"72": "A그룹 | 포트05 자동하강 버튼",
|
||||
"73": "A그룹 | 포트06 자동하강 버튼",
|
||||
"74": "A그룹 | 포트07 자동하강 버튼",
|
||||
"75": "A그룹 | 포트08 자동하강 버튼",
|
||||
"76": "A그룹 | 포트09 자동하강 버튼",
|
||||
"77": "A그룹 | 포트10 자동하강 버튼",
|
||||
"78": "A그룹 | 포트11 자동하강 버튼",
|
||||
"79": "A그룹 | 포트12 자동하강 버튼",
|
||||
"80": "A그룹 | 포트13 자동하강 버튼",
|
||||
"81": "A그룹 | 포트14 자동하강 버튼",
|
||||
"82": "A그룹 | 포트15 자동하강 버튼",
|
||||
"83": "A그룹 | 포트16 자동하강 버튼",
|
||||
"84": "A그룹 | 포트17 자동하강 버튼",
|
||||
"85": "A그룹 | 포트18 자동하강 버튼",
|
||||
"86": "A그룹 | 포트19 자동하강 버튼",
|
||||
"87": "A그룹 | 포트20 자동하강 버튼",
|
||||
"88": "B그룹 | 포트21 자동하강 버튼",
|
||||
"89": "B그룹 | 포트22 자동하강 버튼",
|
||||
"90": "B그룹 | 포트23 자동하강 버튼",
|
||||
"91": "B그룹 | 포트24 자동하강 버튼",
|
||||
"92": "B그룹 | 포트25 자동하강 버튼",
|
||||
"93": "B그룹 | 포트26 자동하강 버튼",
|
||||
"94": "B그룹 | 포트27 자동하강 버튼",
|
||||
"95": "B그룹 | 포트28 자동하강 버튼",
|
||||
"96": "B그룹 | 포트29 자동하강 버튼",
|
||||
"97": "B그룹 | 포트30 자동하강 버튼",
|
||||
"98": "B그룹 | 포트31 자동하강 버튼",
|
||||
"99": "B그룹 | 포트32 자동하강 버튼",
|
||||
"100": "B그룹 | 포트33 자동하강 버튼",
|
||||
"101": "B그룹 | 포트34 자동하강 버튼",
|
||||
"102": "B그룹 | 포트35 자동하강 버튼",
|
||||
"103": "B그룹 | 포트36 자동하강 버튼",
|
||||
"104": "B그룹 | 포트37 자동하강 버튼",
|
||||
"105": "B그룹 | 포트38 자동하강 버튼",
|
||||
"106": "B그룹 | 포트39 자동하강 버튼",
|
||||
"107": "B그룹 | 포트40 자동하강 버튼",
|
||||
"108": "A그룹 | 포트01 수동하강 버튼",
|
||||
"109": "A그룹 | 포트02 수동하강 버튼",
|
||||
"110": "A그룹 | 포트03 수동하강 버튼",
|
||||
"111": "A그룹 | 포트04 수동하강 버튼",
|
||||
"112": "A그룹 | 포트05 수동하강 버튼",
|
||||
"113": "A그룹 | 포트06 수동하강 버튼",
|
||||
"114": "A그룹 | 포트07 수동하강 버튼",
|
||||
"115": "A그룹 | 포트08 수동하강 버튼",
|
||||
"116": "A그룹 | 포트09 수동하강 버튼",
|
||||
"117": "A그룹 | 포트10 수동하강 버튼",
|
||||
"118": "A그룹 | 포트11 수동하강 버튼",
|
||||
"119": "A그룹 | 포트12 수동하강 버튼",
|
||||
"120": "A그룹 | 포트13 수동하강 버튼",
|
||||
"121": "A그룹 | 포트14 수동하강 버튼",
|
||||
"122": "A그룹 | 포트15 수동하강 버튼",
|
||||
"123": "A그룹 | 포트16 수동하강 버튼",
|
||||
"124": "A그룹 | 포트17 수동하강 버튼",
|
||||
"125": "A그룹 | 포트18 수동하강 버튼",
|
||||
"126": "A그룹 | 포트19 수동하강 버튼",
|
||||
"127": "A그룹 | 포트20 수동하강 버튼",
|
||||
"128": "B그룹 | 포트21 수동하강 버튼",
|
||||
"129": "B그룹 | 포트22 수동하강 버튼",
|
||||
"130": "B그룹 | 포트23 수동하강 버튼",
|
||||
"131": "B그룹 | 포트24 수동하강 버튼",
|
||||
"132": "B그룹 | 포트25 수동하강 버튼",
|
||||
"133": "B그룹 | 포트26 수동하강 버튼",
|
||||
"134": "B그룹 | 포트27 수동하강 버튼",
|
||||
"135": "B그룹 | 포트28 수동하강 버튼",
|
||||
"136": "B그룹 | 포트29 수동하강 버튼",
|
||||
"137": "B그룹 | 포트30 수동하강 버튼",
|
||||
"138": "B그룹 | 포트31 수동하강 버튼",
|
||||
"139": "B그룹 | 포트32 수동하강 버튼",
|
||||
"140": "B그룹 | 포트33 수동하강 버튼",
|
||||
"141": "B그룹 | 포트34 수동하강 버튼",
|
||||
"142": "B그룹 | 포트35 수동하강 버튼",
|
||||
"143": "B그룹 | 포트36 수동하강 버튼",
|
||||
"144": "B그룹 | 포트37 수동하강 버튼",
|
||||
"145": "B그룹 | 포트38 수동하강 버튼",
|
||||
"146": "B그룹 | 포트39 수동하강 버튼",
|
||||
"147": "B그룹 | 포트40 수동하강 버튼",
|
||||
"148": "A그룹 | 포트01 수동상승 버튼",
|
||||
"149": "A그룹 | 포트02 수동상승 버튼",
|
||||
"150": "A그룹 | 포트03 수동상승 버튼",
|
||||
"151": "A그룹 | 포트04 수동상승 버튼",
|
||||
"152": "A그룹 | 포트05 수동상승 버튼",
|
||||
"153": "A그룹 | 포트06 수동상승 버튼",
|
||||
"154": "A그룹 | 포트07 수동상승 버튼",
|
||||
"155": "A그룹 | 포트08 수동상승 버튼",
|
||||
"156": "A그룹 | 포트09 수동상승 버튼",
|
||||
"157": "A그룹 | 포트10 수동상승 버튼",
|
||||
"158": "A그룹 | 포트11 수동상승 버튼",
|
||||
"159": "A그룹 | 포트12 수동상승 버튼",
|
||||
"160": "A그룹 | 포트13 수동상승 버튼",
|
||||
"161": "A그룹 | 포트14 수동상승 버튼",
|
||||
"162": "A그룹 | 포트15 수동상승 버튼",
|
||||
"163": "A그룹 | 포트16 수동상승 버튼",
|
||||
"164": "A그룹 | 포트17 수동상승 버튼",
|
||||
"165": "A그룹 | 포트18 수동상승 버튼",
|
||||
"166": "A그룹 | 포트19 수동상승 버튼",
|
||||
"167": "A그룹 | 포트20 수동상승 버튼",
|
||||
"168": "B그룹 | 포트21 수동상승 버튼",
|
||||
"169": "B그룹 | 포트22 수동상승 버튼",
|
||||
"170": "B그룹 | 포트23 수동상승 버튼",
|
||||
"171": "B그룹 | 포트24 수동상승 버튼",
|
||||
"172": "B그룹 | 포트25 수동상승 버튼",
|
||||
"173": "B그룹 | 포트26 수동상승 버튼",
|
||||
"174": "B그룹 | 포트27 수동상승 버튼",
|
||||
"175": "B그룹 | 포트28 수동상승 버튼",
|
||||
"176": "B그룹 | 포트29 수동상승 버튼",
|
||||
"177": "B그룹 | 포트30 수동상승 버튼",
|
||||
"178": "B그룹 | 포트31 수동상승 버튼",
|
||||
"179": "B그룹 | 포트32 수동상승 버튼",
|
||||
"180": "B그룹 | 포트33 수동상승 버튼",
|
||||
"181": "B그룹 | 포트34 수동상승 버튼",
|
||||
"182": "B그룹 | 포트35 수동상승 버튼",
|
||||
"183": "B그룹 | 포트36 수동상승 버튼",
|
||||
"184": "B그룹 | 포트37 수동상승 버튼",
|
||||
"185": "B그룹 | 포트38 수동상승 버튼",
|
||||
"186": "B그룹 | 포트39 수동상승 버튼",
|
||||
"187": "B그룹 | 포트40 수동상승 버튼"
|
||||
},
|
||||
|
||||
"d_write_area": {
|
||||
"0": "A그룹 | 이송모터 자동 목표 이동량",
|
||||
"1": "A그룹 | 이송모터 수동 목표 속도",
|
||||
"2": "B그룹 | 이송모터 자동 목표 이동량",
|
||||
"3": "B그룹 | 이송모터 수동 목표 속도"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import logging
|
||||
from typing import List
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
# 활성화된 웹소켓 커넥션 목록 관리
|
||||
self.active_connections: List[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
logger.info(f"[WS] Client connected. Total active connections: {len(self.active_connections)}")
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
if websocket in self.active_connections:
|
||||
self.active_connections.remove(websocket)
|
||||
logger.info(f"[WS] Client disconnected. Total active connections: {len(self.active_connections)}")
|
||||
|
||||
async def broadcast(self, message: dict):
|
||||
"""
|
||||
접속한 모든 웹 브라우저 클라이언트에게 JSON 데이터를 전송합니다.
|
||||
"""
|
||||
if not self.active_connections:
|
||||
return
|
||||
|
||||
logger.debug(f"[WS BROADCAST] Broadcasting message to {len(self.active_connections)} clients.")
|
||||
disconnected_clients = []
|
||||
for connection in self.active_connections:
|
||||
try:
|
||||
await connection.send_json(message)
|
||||
except Exception as e:
|
||||
logger.warning(f"[WS BROADCAST] Failed to send message to client, marked for cleanup: {e}")
|
||||
disconnected_clients.append(connection)
|
||||
|
||||
# 오류가 난 비정상 커넥션 정리
|
||||
for conn in disconnected_clients:
|
||||
self.disconnect(conn)
|
||||
|
||||
# 전역 싱글톤 매니저 인스턴스
|
||||
ws_manager = ConnectionManager()
|
||||
Reference in New Issue
Block a user