"""
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  IMI Camera Driver (Neptune API)                                             ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃  Date    : 2026-02-08                                                        ┃
┃  Summary : IMI Neptune 카메라 제어 드라이버                                  ┃
┃            - 자동 탐지 및 재연결                                             ┃
┃            - SmBlock 기반 프레임 공유                                        ┃
┃            - Bayer → BGR 디모자이킹                                          ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃  Connection Management                                                       ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
┃                                                                              ┃
┃  Command Queue: put_cmd(action, source) → rx_cmdq → run() loop               ┃
┃                                                                              ┃
┃  ┌─────────────┬────────────────┬─────────────────────────────────────────┐  ┃
┃  │ action      │ source         │ 설명                                    │  ┃
┃  ├─────────────┼────────────────┼─────────────────────────────────────────┤  ┃
┃  │ connect     │ init           │ run() 시작 시 최초 연결                 │  ┃
┃  │ connect     │ device_added   │ 카메라 핫플러그 감지                    │  ┃
┃  │ disconnect  │ device_removed │ 카메라 제거 감지                        │  ┃
┃  │ disconnect  │ usb_unplug     │ USB 분리 콜백                           │  ┃
┃  │ disconnect  │ close_error    │ close() 중 예외 발생                    │  ┃
┃  │ close       │ user_close     │ 사용자 close() 호출                     │  ┃
┃  └─────────────┴────────────────┴─────────────────────────────────────────┘  ┃
┃                                                                              ┃
┃  Auto-Reconnect: 연결 끊김 후 INITIAL_WAIT(3초) 대기, 이후 매 1초 재시도    ┃
┃                                                                              ┃
┃  State: is_opened ──┬── True  → disconnect_counter = 0 (리셋)               ┃
┃                     └── False → disconnect_counter++ (재연결 시도)          ┃
┃                                                                              ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
"""

# ═══════════════════════════════════════════════════════════════════════════════
# Imports
# ═══════════════════════════════════════════════════════════════════════════════
import ctypes
import logging
import multiprocessing
import queue
import time
from dataclasses import dataclass
from threading import Thread
from typing import Optional

import cv2
import numpy as np

from py_alaska import rmi_task, rmi_signal, rmi_run

try:
    from ...sm_block import SmBlock
    from ...gconfig import gconfig
    from .Neptune_API import *
except ImportError:
    import sys
    from pathlib import Path
    sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
    from py_alaska.sm_block import SmBlock
    from py_alaska.gconfig import gconfig
    from py_alaska.drives.imi.Neptune_API import *

# ═══════════════════════════════════════════════════════════════════════════════
# Constants
# ═══════════════════════════════════════════════════════════════════════════════
AUTO_SELECT_MAC = "ff:ff:ff:ff:ff:ff"
HEARTBEAT_TIME = 500        # 카메라 연결 유지 주기 (ms)
INITIAL_WAIT = 3            # 재연결 시도 전 대기 시간 (초)

log = logging.getLogger(__name__)

# ═══════════════════════════════════════════════════════════════════════════════
# Data Classes
# ═══════════════════════════════════════════════════════════════════════════════
@dataclass
class FrameInfo:
    """Frame metadata for camera callback"""
    sm_index: int       # SmBlock index
    rx_sequence: int    # Frame sequence number from camera
    rx_timestamp: float # Frame receive timestamp
    rx_count: int       # Total received frame count
    rx_drop: int        # Total dropped frame count


# ═══════════════════════════════════════════════════════════════════════════════
# Descriptors
# ═══════════════════════════════════════════════════════════════════════════════
class CamProperty:
    """카메라 하드웨어 property descriptor

    - getter: is_opened=True면 하드웨어에서 읽기, 아니면 캐시값 반환
    - setter: is_opened=True면 즉시 적용, 아니면 _lazy_updates에 저장
    """
    def __init__(self, get_func: str = None, set_func: str = None, default=None):
        self.get_func = get_func
        self.set_func = set_func
        self.default = default
        self.attr_name = None

    def __set_name__(self, owner, name):
        self.attr_name = name
        self.private_name = f'_{name}'

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        # is_opened가 아니면 캐시값 반환 (하드웨어 호출 안함)
        if not getattr(obj, 'is_opened', False):
            return getattr(obj, self.private_name, self.default)
        if self.get_func:
            try:
                value = getattr(obj, self.get_func)()
                if value is not None:
                    return value  # 하드웨어 값 반환 (캐시 갱신 안함)
            except Exception as e:
                log.error(f"[CamProperty] {self.attr_name} get failed: {e}")
        return getattr(obj, self.private_name, self.default)

    def __set__(self, obj, value):
        setattr(obj, self.private_name, value)
        if getattr(obj, 'is_opened', False):
            if self.set_func:
                try:
                    getattr(obj, self.set_func)(value)
                except Exception as e:
                    log.error(f"[CamProperty] {self.attr_name} set failed: {e}")
        else:
            if not hasattr(obj, '_lazy_updates'):
                obj._lazy_updates = {}
            obj._lazy_updates[self.attr_name] = value


@rmi_task(name="imi_cam_driver", mode="process", restart=True)
class imi_cam_driver:
    """IMI Camera Control Class"""

    # ═══════════════════════════════════════════════════════════════════════════
    # Class Variables & Descriptors
    # ═══════════════════════════════════════════════════════════════════════════
    DriverInit = False

    fps = CamProperty(get_func='_hw_get_fps', set_func='_hw_set_fps', default=900.0)
    exposure = CamProperty(get_func='_hw_get_exposure', set_func='_hw_set_exposure', default=15000)
    trigger_mode = CamProperty(get_func='_hw_get_trigger', set_func='_hw_set_trigger', default=False)

    # ═══════════════════════════════════════════════════════════════════════════
    # Initialization
    # ═══════════════════════════════════════════════════════════════════════════
    def __init__(self):
        self.is_opened = False
        self._lazy_updates = {}
        self.rx_cmdq = None  # Lazy init in task_loop (pickle 호환)
        self.is_opened = False
        self.rx_drop = 0
        self.rx_count = 0
        self.handle = None  # Lazy init in task_loop (pickle 호환)
        # 캐시 초기값 (CamProperty default와 일치)
        self._fps = 12.0
        self._exposure = 15000
        self._trigger_mode = False
        self.trigger_source = "software"

    # ═══════════════════════════════════════════════════════════════════════════
    # Lifecycle (open / re_connect / close)
    # ═══════════════════════════════════════════════════════════════════════════
    def discovery(self, target_mac: Optional[str] = None) -> Optional[str]:
        """카메라 MAC 주소 반환
        - target_mac=None 또는 'ff:ff:ff:ff:ff:ff': 첫 번째 카메라 자동 선택
        - target_mac=특정값: 탐지 목록에 있으면 반환, 없으면 None
        """
        mac_list = imi_cam_driver.get_mac_list()
        if not mac_list:
            return None

        # 소문자 변환 (대소문자 무시 비교)
        mac_list_lower = [m.lower() for m in mac_list]
        target_lower = target_mac.lower() if target_mac else None

        # 자동 선택 모드
        if target_lower is None or target_lower == AUTO_SELECT_MAC:
            log.info(f"[ImiCamera] discovery: {len(mac_list)} camera(s), auto-select {mac_list[0]}")
            return mac_list[0]

        # 특정 MAC 지정 모드
        if target_lower in mac_list_lower:
            idx = mac_list_lower.index(target_lower)
            log.info(f"[ImiCamera] discovery: {len(mac_list)} camera(s), using {mac_list[idx]}")
            return mac_list[idx]

        log.warning(f"[ImiCamera] discovery: {target_mac} not found in {mac_list}")
        return None

    def re_connect(self):
        if not imi_cam_driver.DriverInit:
            if ntcInit() != ENeptuneError.NEPTUNE_ERR_Success.value:
                log.error("[ImiCamera] Driver initialization failed")
                return False
            imi_cam_driver.DriverInit = True
            log.info("[ImiCamera] Driver initialized")

        if self.is_opened:
            log.debug("[ImiCamera] Already connected")
            return True

        # Clean up existing handle
        if self.handle:
            ntcClose(self.handle)
        self.handle = ctypes.c_void_p(0)

        found_mac = self.discovery(self.mac_address)
        if found_mac is None:
            return False

        # mac_address가 지정된 상태에서 연결 시도
        err = ntcOpen(bytes(found_mac, encoding='utf-8'), ctypes.byref(self.handle))
        if err != ENeptuneError.NEPTUNE_ERR_Success.value:
            log.error(f"[ImiCamera] ntcOpen failed: {self.mac_address} (err={err})")
            return False
        self.is_opened = True

        if ntcSetHeartbeatTime(self.handle, HEARTBEAT_TIME) != ENeptuneError.NEPTUNE_ERR_Success.value:
            log.error("[ImiCamera] ntcSetHeartbeatTime failed")
            return False

        # Callback setup
        self.frameCallBack = ctypes.CFUNCTYPE(None, NEPTUNE_IMAGE, ctypes.c_void_p)(self.RecvFrameCallBack)
        ntcSetFrameCallback(self.handle, self.frameCallBack, self.handle)
        self.callback_drop_func = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(self.RecvFrameDropCallBack)
        ntcSetFrameDropCallback(self.handle, self.callback_drop_func, self.handle)
        self.callback_device_check_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_void_p)(self.RecvDeviceCheckCallBack)
        ntcSetDeviceCheckCallback(self.callback_device_check_func, self.handle)

        self.func_unplug = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(self.RecvUnPlugCallBack)
        ntcSetUnplugCallback(self.handle, self.func_unplug, self.handle)
        self.func_timeout = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(self.RecvTimeoutCallBack)
        ntcSetRecvTimeoutCallback(self.handle, self.func_timeout, self.handle)

        for i, state in enumerate([False, True]):
            ntcSetAcquisition(self.handle, ENeptuneBoolean.NEPTUNE_BOOL_TRUE.value if state else ENeptuneBoolean.NEPTUNE_BOOL_FALSE.value)
        self._lazy_reset()

        self.is_opened = True
        log.info("[ImiCamera] Connected")
        self._emit_connected()

    def close(self) -> None:
        log.info(f"[ImiCamera] Close called, connected={self.is_opened}")
        if not self.is_opened:
            return
        try:
            self.put_cmd("close", "user_close")
            ntcClose(self.handle)
            log.info("[ImiCamera] Closed")
        except Exception as e:
            log.exception(f"[ImiCamera] Error during close: {e}")
            self.put_cmd("disconnect", "close_error")

    # ═══════════════════════════════════════════════════════════════════════════
    # Signal Emit (카메라 상태 알림)
    # ═══════════════════════════════════════════════════════════════════════════
    def _emit_connected(self):
        """카메라 연결 Signal 발행"""
        if hasattr(self, 'runtime') and self.runtime and hasattr(self.runtime, 'signal') and self.runtime.signal:
            data = {
                "source": self.runtime.name,
                "mode": "real",
                "fps": self._fps
            }
            print(f"[{self.runtime.name}] EMIT camera.connected: {data}")
            self.runtime.signal.emit("camera.connected", data=data)
        else:
            print(f"[ImiCamera] _emit_connected: no signal client available")

    def _emit_disconnected(self, reason: str = "stopped"):
        """카메라 연결 해제 Signal 발행"""
        if hasattr(self, 'runtime') and self.runtime and hasattr(self.runtime, 'signal') and self.runtime.signal:
            data = {
                "source": self.runtime.name,
                "reason": reason,
                "captured": self.rx_count,
                "dropped": self.rx_drop
            }
            print(f"[{self.runtime.name}] EMIT camera.disconnected: {data}")
            self.runtime.signal.emit("camera.disconnected", data=data)
        else:
            print(f"[ImiCamera] _emit_disconnected: no signal client available")

    # ═══════════════════════════════════════════════════════════════════════════
    # Task Interface (task_loop / command queue)
    # ═══════════════════════════════════════════════════════════════════════════
    def put_cmd(self, action: str, source: str):
        """명령 큐에 {action, source} 추가"""
        if self.rx_cmdq is not None:
            self.rx_cmdq.put({"action": action, "source": source})

    @rmi_run()
    def run(self):
        log = self.runtime.log
        self.rx_cmdq = multiprocessing.Queue()
        self.handle = ctypes.c_void_p(0)
        self.put_cmd("connect", "init")
        disconnect_counter = 0

        while not self.runtime.should_stop():
            try:
                cmd = self.rx_cmdq.get(timeout=1.0)
                action = cmd["action"]
                source = cmd["source"]
                log.info(f"[ImiCamera] cmd: {action} (from {source})")

                if action == "close":
                    self._emit_disconnected(f"close:{source}")
                    self.is_opened = False
                    break
                elif action == "connect":
                    self.re_connect()
                elif action == "disconnect":
                    self._emit_disconnected(f"disconnect:{source}")
                    self.is_opened = False
            except queue.Empty:
                pass

            # 자동 재연결: 3초 대기 후 매 1초마다 시도
            if self.is_opened:
                disconnect_counter = 0
            else:
                disconnect_counter += 1
                if disconnect_counter >= INITIAL_WAIT:
                    if disconnect_counter % 10 == 0:  # 10초마다 로그
                        log.info(f"[ImiCamera] reconnecting... ({disconnect_counter}s)")
                    self.re_connect()

        self._emit_disconnected("end_of_run")
        log.info("[ImiCamera] run stopped")

    # ═══════════════════════════════════════════════════════════════════════════
    # Hardware Callbacks
    # ═══════════════════════════════════════════════════════════════════════════
    def RecvDeviceCheckCallBack(self, state: int, pContext: Optional[ctypes.c_void_p] = None) -> None:
        log.info(f"[ImiCamera] Device {'added' if state == 0 else 'removed'}")
        if state == 0:
            self.put_cmd("connect", "device_added")
        else:
            self.put_cmd("disconnect", "device_removed")
        return None

    def RecvUnPlugCallBack(self, _: Optional[ctypes.c_void_p] = None) -> None:
        """USB 분리 콜백"""
        self.put_cmd("disconnect", "unplug")

    def RecvTimeoutCallBack(self, _: Optional[ctypes.c_void_p] = None) -> None:
        log.warning("[ImiCamera] Frame timeout")

    def RecvFrameCallBack(self, pImage: NEPTUNE_IMAGE, pContext: Optional[ctypes.c_void_p] = None) -> Optional[np.ndarray]:
        self.rx_count += 1

        if self.smblock is None:
            log.warning("[ImiCamera] SmBlock not initialized, frame dropped")
            return None

        # Zero-copy: SmBlock 버퍼에 직접 쓰기
        index = self.smblock.alloc()
        if index == -1:
            self.rx_drop += 1
            return None

        try:
            # 카메라 버퍼 → numpy view (복사 없음)
            buffer_type = ctypes.c_uint8 * pImage.uiSize
            buffer_ptr = ctypes.cast(pImage.pData, ctypes.POINTER(buffer_type))
            rx_frame = np.frombuffer(buffer_ptr.contents, dtype=np.uint8)

            # SmBlock 버퍼 참조 획득
            dst_buffer = self.smblock.get_buffer(index)
            sm_channels = self.smblock.shape[2] if len(self.smblock.shape) > 2 else 1

            if sm_channels == 1:
                # Grayscale 출력
                if pImage.uiBitDepth != 8:
                    dst_buffer[:] = rx_frame.reshape(pImage.uiHeight, pImage.uiWidth)
                else:
                    cv2.cvtColor(rx_frame.reshape(pImage.uiHeight, pImage.uiWidth, -1),
                                 cv2.COLOR_BGR2GRAY, dst=dst_buffer)
            else:
                # BGR 출력 - Bayer 디코딩을 SmBlock에 직접 수행
                if pImage.uiBitDepth != 8:
                    cv2.cvtColor(rx_frame.reshape(pImage.uiHeight, pImage.uiWidth),
                                 cv2.COLOR_GRAY2BGR, dst=dst_buffer)
                else:
                    cv2.cvtColor(rx_frame.reshape(pImage.uiHeight, pImage.uiWidth, -1),
                                 cv2.COLOR_BayerGB2BGR, dst=dst_buffer)

            # Create FrameInfo and call callback
            frame_info = FrameInfo(
                sm_index=index,
                rx_sequence=self.rx_count,
                rx_timestamp=time.time(),
                rx_count=self.rx_count,
                rx_drop=self.rx_drop
            )

            if hasattr(self, 'nextTask') and self.nextTask is not None:
                self.nextTask.on_input.nowait(frame_info)  # 비동기 호출 (타임아웃 방지)
            else:
                print(" no next task defined ")
                self.smblock.mfree(index)  # nextTask 없으면 블록 해제
        except Exception as e:
            self.smblock.mfree(index)  # 예외 발생 시 블록 해제
            log.exception(f"[ImiCamera] Frame processing error: {e}")

    def RecvFrameDropCallBack(self, pContext: Optional[ctypes.c_void_p] = None) -> None:
        self.rx_drop += 1

    # ═══════════════════════════════════════════════════════════════════════════
    # Hardware I/O (CamProperty descriptors용)
    # ═══════════════════════════════════════════════════════════════════════════

    def one_shot(self):
        """소프트웨어 트리거 1회 발생"""
        if not self.is_opened :
            log.warning("[ImiCamera] oneShot_trigger called but not connected")
            return

        if self.trigger_source != "software":
            log.warning("[ImiCamera] oneShot_trigger called but trigger_source is not 'software'")
            return

        err = ntcRunSWTrigger(self.handle)
        if err != ENeptuneError.NEPTUNE_ERR_Success.value:
            log.error(f"[ImiCamera] ntcRunSWTrigger failed (err={err})")

    def _hw_get_fps(self) -> float:
        emFPS = ctypes.c_int32()
        dbFPS = ctypes.c_double()
        err = ntcGetFrameRate(self.handle, ctypes.byref(emFPS), ctypes.byref(dbFPS))
        if err == 0:
            log.debug(f"[ImiCamera] Current FPS: {dbFPS.value}")
            return dbFPS.value
        log.error(f"[ImiCamera] ntcGetFrameRate failed: err={err}")
        return None  # 에러 시 None 반환 → 캐시값 유지

    def _hw_set_fps(self, value: float):
        ntcSetAcquisition(self.handle, ENeptuneBoolean.NEPTUNE_BOOL_FALSE.value)
        emFPS = ctypes.c_int32(ENeptuneFrameRate.FPS_VALUE.value)
        dbFPS = ctypes.c_double(value)
        err = ntcSetFrameRate(self.handle, emFPS.value, dbFPS.value)
        if err != 0:
            log.error(f"[ImiCamera] ntcSetFrameRate failed: fps={value}, err={err}")
        ntcSetAcquisition(self.handle, ENeptuneBoolean.NEPTUNE_BOOL_TRUE.value)

    def _hw_get_exposure(self) -> int:
        pui = ctypes.c_uint32()
        err = ntcGetExposureTime(self.handle, ctypes.byref(pui))
        if err != 0:
            log.error(f"[ImiCamera] ntcGetExposureTime failed: err={err}")
            return None  # 에러 시 None 반환 → 캐시값 유지
        return pui.value

    def _hw_set_exposure(self, value: int):
        pui = ctypes.c_uint32(value)
        if ntcSetExposureTime(self.handle, pui) != 0:
            log.error("[ImiCamera] ntcSetExposureTime failed")

    def _hw_get_trigger(self) -> bool:
        neptune_trigger = NEPTUNE_TRIGGER()
        err = ntcGetTrigger(self.handle, ctypes.pointer(neptune_trigger))
        if err != 0:
            log.error(f"[ImiCamera] ntcGetTrigger failed: err={err}")
            return None  # 에러 시 None 반환 → 캐시값 유지
        return neptune_trigger.OnOff == ENeptuneBoolean.NEPTUNE_BOOL_TRUE.value


    def _hw_set_trigger(self, enabled: bool):
        st_trigger = NEPTUNE_TRIGGER()
        if self.trigger_source == "hardware":
            st_trigger.Source = ENeptuneTriggerSource.NEPTUNE_TRIGGER_SOURCE_LINE1.value
        else:
            st_trigger.Source = ENeptuneTriggerSource.NEPTUNE_TRIGGER_SOURCE_SW.value
            print( "********************** SOFTWARE TRIGGER MODE SET **********************" )
        st_trigger.Mode = ENeptuneTriggerMode.NEPTUNE_TRIGGER_MODE_0.value
        st_trigger.Polarity = ENeptunePolarity.NEPTUNE_POLARITY_FALLINGEDGE.value
        st_trigger.OnOff = ENeptuneBoolean.NEPTUNE_BOOL_TRUE.value if enabled else ENeptuneBoolean.NEPTUNE_BOOL_FALSE.value

        # trigger on: 고속 FPS 설정, trigger off: 캐시된 사용자 FPS 유지
        if enabled:
            self._hw_set_fps(300.0)
        else:
            self._hw_set_fps(self._fps)

        if ntcSetTrigger(self.handle, st_trigger) != ENeptuneError.NEPTUNE_ERR_Success.value:
            log.error("[ImiCamera] ntcSetTrigger failed")

    # ═══════════════════════════════════════════════════════════════════════════
    # Internal Helpers
    # ═══════════════════════════════════════════════════════════════════════════
    def clear_counters(self):
        self.rx_count = 0
        self.rx_drop = 0

    def _lazy_apply(self):
        """pending된 설정만 적용"""
        for attr, value in self._lazy_updates.items():
            log.info(f"[ImiCamera] Lazy applying {attr}={value}")
            setattr(self, attr, value)
        self._lazy_updates.clear()

    def _lazy_reset(self):
        """하드웨어 리셋 후 모든 캐시된 값을 재설정 (최적화: 한번만 stop/start)"""
        if not self.is_opened:
            log.warning("[ImiCamera] _lazy_reset called but not opened")
            return

        # 일시스톱
        ntcSetAcquisition(self.handle, ENeptuneBoolean.NEPTUNE_BOOL_FALSE.value)

        # FPS 설정
        emFPS = ctypes.c_int32(ENeptuneFrameRate.FPS_VALUE.value)
        dbFPS = ctypes.c_double(self._fps)
        err = ntcSetFrameRate(self.handle, emFPS.value, dbFPS.value)
        if err != 0:
            log.error(f"[ImiCamera] ntcSetFrameRate failed: fps={self._fps}, err={err}")

        # Exposure 설정
        pui = ctypes.c_uint32(self._exposure)
        if ntcSetExposureTime(self.handle, pui) != 0:
            log.error("[ImiCamera] ntcSetExposureTime failed")

        # Trigger 설정
        st_trigger = NEPTUNE_TRIGGER()
        if self.trigger_source == "hardware":
            st_trigger.Source = ENeptuneTriggerSource.NEPTUNE_TRIGGER_SOURCE_LINE1.value
        else:
            st_trigger.Source = ENeptuneTriggerSource.NEPTUNE_TRIGGER_SOURCE_SW.value
        st_trigger.Mode = ENeptuneTriggerMode.NEPTUNE_TRIGGER_MODE_0.value
        st_trigger.Polarity = ENeptunePolarity.NEPTUNE_POLARITY_FALLINGEDGE.value
        st_trigger.OnOff = ENeptuneBoolean.NEPTUNE_BOOL_TRUE.value if self._trigger_mode else ENeptuneBoolean.NEPTUNE_BOOL_FALSE.value
        if ntcSetTrigger(self.handle, st_trigger) != ENeptuneError.NEPTUNE_ERR_Success.value:
            log.error("[ImiCamera] ntcSetTrigger failed")

        # 한번만 start
        ntcSetAcquisition(self.handle, ENeptuneBoolean.NEPTUNE_BOOL_TRUE.value)
        log.info(f"[ImiCamera] Settings applied: fps={self._fps}, exposure={self._exposure}, trigger={self._trigger_mode}")

    # ═══════════════════════════════════════════════════════════════════════════
    # Static Utilities
    # ═══════════════════════════════════════════════════════════════════════════
    @staticmethod
    def get_mac_list() -> list[str]:
        """Return list of connected camera MAC addresses"""
        numberOfCamera = ctypes.c_uint32(0)
        err = ntcGetCameraCount(ctypes.pointer(numberOfCamera))
        if err != ENeptuneError.NEPTUNE_ERR_Success.value or numberOfCamera.value == 0:
            log.debug(f"[get_mac_list] no camera (err={err})")
            return []

        cam_count = numberOfCamera.value
        info = (NEPTUNE_CAM_INFO * cam_count)()
        err = ntcGetCameraInfo(info, cam_count)

        if err != ENeptuneError.NEPTUNE_ERR_Success.value:
            log.error(f"[get_mac_list] ntcGetCameraInfo failed: err={err}")
            return []

        mac_list = [info[i].strMAC.decode('utf-8') for i in range(cam_count)]
        log.info(f"[get_mac_list] Found {cam_count} camera(s): {mac_list}")
        return mac_list


