#include <algorithm>
#include <cmath>
#include <deque>
#include <string>
#include <utility>
#include <vector>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

#include "MvCameraControl.h"
#include "camera_info_manager/camera_info_manager.hpp"
#include "image_transport/image_transport.hpp"
#include "rclcpp/logging.hpp"
#include "rclcpp/utilities.hpp"
#include "std_msgs/msg/float64.hpp"

namespace hik_camera_ros2_driver
{

struct TimeStamp {
  int64_t high;
  int64_t low;
};

class HikCameraRos2DriverNode : public rclcpp::Node
{
public:
  explicit HikCameraRos2DriverNode(const rclcpp::NodeOptions & options)
  : Node("hik_camera_ros2_driver", options)
  {
    RCLCPP_INFO(this->get_logger(), "Starting HikCameraRos2DriverNode!");

    initializeCamera();
    declareParameters();
    startCamera();
    initSharedMemory();
    initSync();

    params_callback_handle_ = this->add_on_set_parameters_callback(
      std::bind(&HikCameraRos2DriverNode::dynamicParametersCallback, this, std::placeholders::_1));

    capture_thread_ = std::thread(&HikCameraRos2DriverNode::captureLoop, this);
  }

  ~HikCameraRos2DriverNode() override
  {
    if (capture_thread_.joinable()) {
      capture_thread_.join();
    }
    if (camera_handle_) {
      MV_CC_StopGrabbing(camera_handle_);
      MV_CC_CloseDevice(camera_handle_);
      MV_CC_DestroyHandle(&camera_handle_);
    }
    if (pointt_ != nullptr && pointt_ != MAP_FAILED) {
      munmap(pointt_, sizeof(TimeStamp));
      pointt_ = nullptr;
    }
    RCLCPP_INFO(this->get_logger(), "HikCameraRos2DriverNode destroyed!");
  }

private:
  bool initializeCamera()
  {
    // serial_number을 먼저 선언하여 카메라 선택에 사용
    serial_number_ = this->declare_parameter("serial_number", std::string(""));

    MV_CC_DEVICE_INFO_LIST device_list;

    // USB + GigE 동시 열거
    while (rclcpp::ok()) {
      n_ret_ = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &device_list);
      if (n_ret_ != MV_OK) {
        RCLCPP_ERROR(this->get_logger(), "Failed to enumerate devices, retrying...");
        std::this_thread::sleep_for(std::chrono::seconds(1));
      } else if (device_list.nDeviceNum == 0) {
        RCLCPP_ERROR(this->get_logger(), "No camera found, retrying...");
        std::this_thread::sleep_for(std::chrono::seconds(1));
      } else {
        RCLCPP_INFO(this->get_logger(), "Found camera count = %d", device_list.nDeviceNum);
        break;
      }
    }

    // 시리얼 번호 기반 카메라 선택
    unsigned int target_index = 0;
    if (!serial_number_.empty()) {
      bool found = false;
      for (unsigned int i = 0; i < device_list.nDeviceNum; i++) {
        MV_CC_DEVICE_INFO * info = device_list.pDeviceInfo[i];
        std::string serial;
        if (info->nTLayerType == MV_USB_DEVICE) {
          serial = std::string(
            reinterpret_cast<char *>(info->SpecialInfo.stUsb3VInfo.chSerialNumber));
        } else if (info->nTLayerType == MV_GIGE_DEVICE) {
          serial = std::string(
            reinterpret_cast<char *>(info->SpecialInfo.stGigEInfo.chSerialNumber));
        }
        if (serial == serial_number_) {
          target_index = i;
          found = true;
          RCLCPP_INFO(this->get_logger(), "Matched camera serial: %s", serial.c_str());
          break;
        }
      }
      if (!found) {
        RCLCPP_FATAL(
          this->get_logger(), "Camera with serial [%s] not found!", serial_number_.c_str());
        rclcpp::shutdown();
        return false;
      }
    }

    n_ret_ = MV_CC_CreateHandle(&camera_handle_, device_list.pDeviceInfo[target_index]);
    if (n_ret_ != MV_OK) {
      RCLCPP_ERROR(this->get_logger(), "Failed to create camera handle!");
      return false;
    }

    n_ret_ = MV_CC_OpenDevice(camera_handle_);
    if (n_ret_ != MV_OK) {
      RCLCPP_ERROR(this->get_logger(), "Failed to open camera device!");
      return false;
    }

    // Get camera information
    n_ret_ = MV_CC_GetImageInfo(camera_handle_, &img_info_);
    if (n_ret_ != MV_OK) {
      RCLCPP_ERROR(this->get_logger(), "Failed to get camera image info!");
      return false;
    }

    // Init convert param
    image_msg_.data.reserve(img_info_.nHeightMax * img_info_.nWidthMax * 3);
    convert_param_.nWidth = img_info_.nWidthValue;
    convert_param_.nHeight = img_info_.nHeightValue;
    convert_param_.enDstPixelType = PixelType_Gvsp_RGB8_Packed;

    return true;
  }

  void declareParameters()
  {
    rcl_interfaces::msg::ParameterDescriptor param_desc;
    MVCC_FLOATVALUE f_value;
    param_desc.integer_range.resize(1);
    param_desc.integer_range[0].step = 1;

    // Trigger mode
    trigger_enable_ = this->declare_parameter("trigger_enable", false);
    use_trigger_timestamp_ = this->declare_parameter("use_trigger_timestamp", false);
    enable_interval_log_ = this->declare_parameter("enable_interval_log", false);
    dev_clock_window_sec_ = this->declare_parameter("dev_clock_window_sec", 10.0);
    dev_clock_recalib_interval_sec_ =
      this->declare_parameter("dev_clock_recalib_interval_sec", 2.0);

    if (trigger_enable_) {
      // 하드웨어 트리거: 프레임률은 트리거 신호가 제어
      MV_CC_SetBoolValue(camera_handle_, "AcquisitionFrameRateEnable", false);
      MV_CC_SetEnumValue(camera_handle_, "TriggerMode", 1);          // On
      MV_CC_SetEnumValue(camera_handle_, "TriggerSource", MV_TRIGGER_SOURCE_LINE0);
      RCLCPP_INFO(this->get_logger(), "Hardware trigger enabled (LINE0)");
    } else {
      MV_CC_SetEnumValue(camera_handle_, "TriggerMode", 0);          // Off

      // Acquisition frame rate (프리런 모드에서만 사용)
      param_desc.description = "Acquisition frame rate in Hz";
      MV_CC_GetFloatValue(camera_handle_, "AcquisitionFrameRate", &f_value);
      param_desc.integer_range[0].from_value = static_cast<int64_t>(f_value.fMin);
      param_desc.integer_range[0].to_value = static_cast<int64_t>(f_value.fMax);
      double acquisition_frame_rate =
        this->declare_parameter("acquisition_frame_rate", 165.0, param_desc);
      MV_CC_SetBoolValue(camera_handle_, "AcquisitionFrameRateEnable", true);
      MV_CC_SetFloatValue(camera_handle_, "AcquisitionFrameRate", acquisition_frame_rate);
      RCLCPP_INFO(this->get_logger(), "Acquisition frame rate: %f", acquisition_frame_rate);
    }

    // 노출/게인 동기화 (마스터-슬레이브) — exposure/gain 블록보다 먼저 선언해야
    // applyExposureMode()/applyGainMode()가 이 값을 보고 분기할 수 있음.
    // independent: 이 카메라 단독으로 노출/게인 결정 (기존 동작, 기본값)
    // master: 실제 적용된 노출/게인을 ae_exposure_time_us / ae_gain_db 토픽으로 발행
    // slave: sync_master_camera_ns 의 master 토픽을 구독해 그 값을 그대로 적용
    param_desc.description = "노출/게인 동기화 역할: independent / master / slave";
    sync_role_ = this->declare_parameter("sync_role", std::string("independent"));
    param_desc.description = "sync_role=slave일 때 구독할 마스터 카메라의 camera_name (예: \"cam2\")";
    sync_master_camera_ns_ = this->declare_parameter("sync_master_camera_ns", std::string(""));

    // Exposure — 모든 파라미터를 항상 선언하여 런타임 모드 전환 지원
    MV_CC_GetFloatValue(camera_handle_, "ExposureTime", &f_value);
    exposure_auto_ = this->declare_parameter("exposure_auto", false);

    param_desc.description = "Auto exposure target brightness (0-255)";
    param_desc.integer_range[0].from_value = 0;
    param_desc.integer_range[0].to_value = 255;
    exposure_auto_target_brightness_ =
      this->declare_parameter("exposure_auto_target_brightness", 128, param_desc);

    param_desc.description = "Auto exposure lower limit in microseconds";
    param_desc.integer_range[0].from_value = static_cast<int64_t>(f_value.fMin);
    param_desc.integer_range[0].to_value = static_cast<int64_t>(f_value.fMax);
    exposure_auto_min_ = this->declare_parameter("exposure_auto_min", 100.0, param_desc);

    param_desc.description = "Auto exposure upper limit in microseconds";
    exposure_auto_max_ = this->declare_parameter("exposure_auto_max", 10000.0, param_desc);

    param_desc.description = "Manual exposure time in microseconds";
    exposure_time_ = this->declare_parameter("exposure_time", 5000, param_desc);

    // 소프트웨어 AE/AG — 카메라 온보드 ExposureAuto/GainAuto는 항상 풀프레임을 측광한다.
    // 이 SDK(hikSDK/include)에는 캡처 AOI(MV_CC_Set/GetAOIoffsetX/Y, 이미지 자체를 자름)와
    // 별개로 "측광에만 쓰는 ROI"를 지정하는 GenICam 노드가 없다 (AutoFunctionAOI 계열
    // 탐색했으나 없음). 하늘을 제외한 하단부만 측광하려면(4번 질문, camera_exposure_design_notes.md
    // 4절) 온보드 auto를 끄고 소프트웨어에서 직접 ExposureTime/Gain을 계산해 써야 한다.
    param_desc.description =
      "true면 온보드 Continuous auto 대신, ae_roi_top_ratio로 제한한 영역의 퍼센타일 밝기로 "
      "노출/게인을 소프트웨어에서 직접 계산해 적용한다";
    use_software_ae_ = this->declare_parameter("use_software_ae", false);

    param_desc.description =
      "측광에서 제외할 프레임 상단 비율 (0.5 = 하단 절반만 측광, 하늘 제외 용도)";
    ae_roi_top_ratio_ = this->declare_parameter("ae_roi_top_ratio", 0.0);

    param_desc.description = "측광 ROI 내 목표 퍼센타일 (0~100)";
    ae_target_percentile_ = this->declare_parameter("ae_target_percentile", 70.0);

    param_desc.description = "ae_target_percentile 지점의 목표 DN (0~255)";
    ae_target_dn_ = this->declare_parameter("ae_target_dn", 130);

    param_desc.description = "포화 방지용 퍼센타일 (0~100)";
    ae_saturation_percentile_ = this->declare_parameter("ae_saturation_percentile", 98.0);

    param_desc.description = "ae_saturation_percentile 지점이 이 DN을 넘지 않도록 제한";
    ae_saturation_dn_ = this->declare_parameter("ae_saturation_dn", 245);

    param_desc.description = "소프트웨어 AE/AG 1프레임당 보정 비율 (0~1, 클수록 빨리 수렴하되 진동 위험)";
    ae_step_gain_ = this->declare_parameter("ae_step_gain", 0.5);

    if (use_software_ae_) {
      soft_ae_exposure_us_ = static_cast<double>(exposure_time_);
    }

    applyExposureMode();

    // Gain
    param_desc.description =
      "true면 게인을 자동 조절한다 (use_software_ae=false: 온보드 Continuous, "
      "use_software_ae=true: 위 소프트웨어 AE/AG 루프가 함께 조절)";
    gain_auto_ = this->declare_parameter("gain_auto", false);

    param_desc.description = "Gain";
    MV_CC_GetFloatValue(camera_handle_, "Gain", &f_value);
    param_desc.integer_range[0].from_value = static_cast<int64_t>(f_value.fMin);
    param_desc.integer_range[0].to_value = static_cast<int64_t>(f_value.fMax);
    gain_ = this->declare_parameter("gain", static_cast<double>(f_value.fCurValue), param_desc);

    param_desc.description =
      "자동 게인 상한 [dB] — 하드웨어 실측 상한(카메라별 상이, 대략 16.9dB)보다 낮게 잡아서 "
      "노이즈를 제한하는 용도. gain 파라미터 자체의 상한(위 range)과는 별개 값";
    param_desc.integer_range[0].from_value = 0;
    param_desc.integer_range[0].to_value = static_cast<int64_t>(f_value.fMax);
    gain_auto_max_db_ = this->declare_parameter("gain_auto_max_db", 12.0, param_desc);

    if (use_software_ae_) {
      soft_ae_gain_db_ = gain_;
    }

    applyGainMode();

    // White balance — 기본은 수동(Off) + 고정 R/G/B 비율.
    // 이유: 카메라별로 독립 Continuous AWB를 켜두면 두 카메라가 서로 다른 화각을 보고
    // 각자 다른 색으로 수렴해서, 겹치는 영역의 색감이 안 맞게 된다. 두 카메라에
    // 동일한 balance_ratio_red/green/blue 값을 넣어야 겹치는 부분 색이 일치한다.
    param_desc.description = "Auto white balance (Continuous). false면 수동 R/G/B 비율 사용";
    balance_white_auto_ = this->declare_parameter("balance_white_auto", false);

    MVCC_INTVALUE wb_range;
    MV_CC_GetBalanceRatioRed(camera_handle_, &wb_range);
    param_desc.integer_range[0].from_value = static_cast<int64_t>(wb_range.nMin);
    param_desc.integer_range[0].to_value = static_cast<int64_t>(wb_range.nMax);
    param_desc.description = "Manual white balance ratio - Red (balance_white_auto=false일 때만 적용)";
    int balance_ratio_red = this->declare_parameter("balance_ratio_red", 1024, param_desc);
    param_desc.description = "Manual white balance ratio - Green (balance_white_auto=false일 때만 적용)";
    int balance_ratio_green = this->declare_parameter("balance_ratio_green", 1024, param_desc);
    param_desc.description = "Manual white balance ratio - Blue (balance_white_auto=false일 때만 적용)";
    int balance_ratio_blue = this->declare_parameter("balance_ratio_blue", 1024, param_desc);

    if (balance_white_auto_) {
      MV_CC_SetEnumValue(camera_handle_, "BalanceWhiteAuto", MV_BALANCEWHITE_AUTO_CONTINUOUS);
      RCLCPP_INFO(this->get_logger(), "White balance: Continuous auto");
    } else {
      MV_CC_SetEnumValue(camera_handle_, "BalanceWhiteAuto", MV_BALANCEWHITE_AUTO_OFF);
      MV_CC_SetBalanceRatioRed(camera_handle_, static_cast<unsigned int>(balance_ratio_red));
      MV_CC_SetBalanceRatioGreen(camera_handle_, static_cast<unsigned int>(balance_ratio_green));
      MV_CC_SetBalanceRatioBlue(camera_handle_, static_cast<unsigned int>(balance_ratio_blue));
      RCLCPP_INFO(
        this->get_logger(), "White balance: Manual R=%d G=%d B=%d",
        balance_ratio_red, balance_ratio_green, balance_ratio_blue);
    }

    int status;

    // ADC Bit Depth
    param_desc.description = "ADC Bit Depth";
    param_desc.additional_constraints = "Supported values: Bits_8, Bits_12";
    std::string adc_bit_depth = this->declare_parameter("adc_bit_depth", "Bits_8", param_desc);
    status = MV_CC_SetEnumValueByString(camera_handle_, "ADCBitDepth", adc_bit_depth.c_str());
    if (status == MV_OK) {
      RCLCPP_INFO(this->get_logger(), "ADC Bit Depth set to %s", adc_bit_depth.c_str());
    } else {
      RCLCPP_ERROR(this->get_logger(), "Failed to set ADC Bit Depth, status = %d", status);
    }

    // Pixel format
    param_desc.description = "Pixel Format";
    std::string pixel_format = this->declare_parameter("pixel_format", "RGB8Packed", param_desc);
    status = MV_CC_SetEnumValueByString(camera_handle_, "PixelFormat", pixel_format.c_str());
    if (status == MV_OK) {
      RCLCPP_INFO(this->get_logger(), "Pixel Format set to %s", pixel_format.c_str());
    } else {
      RCLCPP_ERROR(this->get_logger(), "Failed to set Pixel Format, status = %d", status);
    }
  }

  void startCamera()
  {
    bool use_sensor_data_qos = this->declare_parameter("use_sensor_data_qos", true);
    camera_name_ = this->declare_parameter("camera_name", "camera");
    frame_id_ = this->declare_parameter("frame_id", camera_name_ + "_optical_frame");
    camera_topic_ = this->declare_parameter("camera_topic", camera_name_ + "/image");

    auto qos = use_sensor_data_qos ? rmw_qos_profile_sensor_data : rmw_qos_profile_default;
    camera_pub_ = image_transport::create_camera_publisher(this, camera_topic_, qos);

    MV_CC_StartGrabbing(camera_handle_);

    // Load camera info
    camera_info_manager_ =
      std::make_unique<camera_info_manager::CameraInfoManager>(this, camera_name_);
    auto camera_info_url = this->declare_parameter(
      "camera_info_url", "package://hik_camera_ros2_driver/config/camera_info.yaml");
    if (camera_info_manager_->validateURL(camera_info_url)) {
      camera_info_manager_->loadCameraInfo(camera_info_url);
      camera_info_msg_ = camera_info_manager_->getCameraInfo();
    } else {
      RCLCPP_WARN(this->get_logger(), "Invalid camera info URL: %s", camera_info_url.c_str());
    }
  }

  void initSharedMemory()
  {
    if (!use_trigger_timestamp_) {
      return;
    }

    const char * user_name = getlogin();
    if (user_name == nullptr) {
      RCLCPP_WARN(this->get_logger(), "getlogin() failed, falling back to system time");
      return;
    }

    std::string path = "/home/" + std::string(user_name) + "/timeshare";
    int fd = open(path.c_str(), O_RDWR);
    if (fd == -1) {
      RCLCPP_WARN(
        this->get_logger(),
        "Failed to open shared memory file: %s (LiDAR driver may not be running yet)",
        path.c_str());
      return;
    }

    pointt_ = reinterpret_cast<TimeStamp *>(
      mmap(nullptr, sizeof(TimeStamp), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
    close(fd);

    if (pointt_ == MAP_FAILED) {
      RCLCPP_WARN(this->get_logger(), "mmap failed, falling back to system time");
      pointt_ = nullptr;
      return;
    }

    RCLCPP_INFO(this->get_logger(), "Shared memory timestamp enabled: %s", path.c_str());
  }

  // 노출/게인 마스터-슬레이브 동기화. 카메라별 실제 적용값(fExposureTime/fGain,
  // MV_FRAME_OUT_INFO_EX에 매 프레임 이미 들어있음 — captureLoop 참조)을 퍼블리시/구독한다.
  // publisher는 role과 무관하게 항상 만들어서, 굳이 master로 지정하지 않아도 다른 노드가
  // 관찰용으로 구독할 수 있게 한다.
  void initSync()
  {
    ae_exposure_pub_ = this->create_publisher<std_msgs::msg::Float64>(
      camera_name_ + "/ae_exposure_time_us", 10);
    ae_gain_pub_ = this->create_publisher<std_msgs::msg::Float64>(
      camera_name_ + "/ae_gain_db", 10);

    if (sync_role_ == "slave") {
      if (sync_master_camera_ns_.empty()) {
        RCLCPP_ERROR(this->get_logger(),
          "sync_role=slave 인데 sync_master_camera_ns가 비어 있음 — 마스터를 구독할 수 없음");
        return;
      }
      const std::string prefix = "/" + sync_master_camera_ns_;
      ae_exposure_sub_ = this->create_subscription<std_msgs::msg::Float64>(
        prefix + "/ae_exposure_time_us", 10,
        [this](const std_msgs::msg::Float64::SharedPtr msg) {
          exposure_time_ = static_cast<int>(std::lround(msg->data));
          MV_CC_SetFloatValue(camera_handle_, "ExposureTime", static_cast<float>(msg->data));
        });
      ae_gain_sub_ = this->create_subscription<std_msgs::msg::Float64>(
        prefix + "/ae_gain_db", 10,
        [this](const std_msgs::msg::Float64::SharedPtr msg) {
          gain_ = msg->data;
          MV_CC_SetFloatValue(camera_handle_, "Gain", static_cast<float>(msg->data));
        });
      RCLCPP_INFO(this->get_logger(), "Sync: slave of %s", prefix.c_str());
    } else if (sync_role_ == "master") {
      RCLCPP_INFO(this->get_logger(),
        "Sync: master (publishing %s/ae_exposure_time_us, %s/ae_gain_db)",
        camera_name_.c_str(), camera_name_.c_str());
    }
  }

  void applyExposureMode()
  {
    if (sync_role_ == "slave") {
      // 슬레이브는 마스터가 보내주는 값을 그대로 적용할 뿐, 자체 auto 로직을 돌리지 않는다
      // (initSync()의 구독 콜백이 실제 적용을 담당). 여기서는 초기값만 세팅.
      MV_CC_SetEnumValue(camera_handle_, "ExposureAuto", 0);
      MV_CC_SetFloatValue(camera_handle_, "ExposureTime", static_cast<float>(exposure_time_));
      RCLCPP_INFO(this->get_logger(), "Exposure: slave mode, waiting for master sync");
      return;
    }
    if (use_software_ae_) {
      // 온보드 auto는 ROI 측광을 지원하지 않으므로 항상 Off로 두고, 실제 조절은
      // runSoftwareAeStep()이 매 프레임 ExposureTime을 직접 써서 수행한다.
      MV_CC_SetEnumValue(camera_handle_, "ExposureAuto", 0);
      MV_CC_SetFloatValue(camera_handle_, "ExposureTime", static_cast<float>(soft_ae_exposure_us_));
      RCLCPP_INFO(this->get_logger(),
        "Software AE: %s, ROI top-exclude=%.2f, target p%.0f=DN%d, range=[%.0f, %.0f] us",
        exposure_auto_ ? "ON" : "OFF (fixed)", ae_roi_top_ratio_,
        ae_target_percentile_, ae_target_dn_, exposure_auto_min_, exposure_auto_max_);
      return;
    }
    if (exposure_auto_) {
      MV_CC_SetEnumValue(camera_handle_, "ExposureAuto", 2);  // Continuous
      MV_CC_SetIntValue(camera_handle_, "AutoTargetBrightness",
        static_cast<unsigned int>(exposure_auto_target_brightness_));
      MV_CC_SetIntValue(camera_handle_, "AutoExposureTimeLowerLimit",
        static_cast<unsigned int>(exposure_auto_min_));
      MV_CC_SetIntValue(camera_handle_, "AutoExposureTimeUpperLimit",
        static_cast<unsigned int>(exposure_auto_max_));
      RCLCPP_INFO(this->get_logger(),
        "Auto exposure: ON, brightness=%d, range=[%.0f, %.0f] us",
        exposure_auto_target_brightness_, exposure_auto_min_, exposure_auto_max_);
    } else {
      MV_CC_SetEnumValue(camera_handle_, "ExposureAuto", 0);  // Off
      MV_CC_SetFloatValue(camera_handle_, "ExposureTime", static_cast<float>(exposure_time_));
      RCLCPP_INFO(this->get_logger(), "Manual exposure: %d us", exposure_time_);
    }
  }

  void applyGainMode()
  {
    if (sync_role_ == "slave") {
      MV_CC_SetEnumValue(camera_handle_, "GainAuto", 0);
      MV_CC_SetFloatValue(camera_handle_, "Gain", static_cast<float>(gain_));
      RCLCPP_INFO(this->get_logger(), "Gain: slave mode, waiting for master sync");
      return;
    }
    if (use_software_ae_) {
      MV_CC_SetEnumValue(camera_handle_, "GainAuto", 0);
      MV_CC_SetFloatValue(camera_handle_, "Gain", static_cast<float>(soft_ae_gain_db_));
      RCLCPP_INFO(this->get_logger(),
        "Software AG: %s, range=[0, %.1f] dB", gain_auto_ ? "ON" : "OFF (fixed)",
        gain_auto_max_db_);
      return;
    }
    if (gain_auto_) {
      MV_CC_SetEnumValue(camera_handle_, "GainAuto", 2);  // Continuous
      // AutoGainLowerLimit/UpperLimit은 AutoExposureTimeLowerLimit/UpperLimit과 같은 명명
      // 규칙을 따른다고 가정한 값이다 — 이 카메라의 실제 GenICam 노드맵에서 검증된 적은
      // 없음. 실패 시 아래 WARN 로그로 바로 드러나므로, 뜨면 MVS Feature Tree에서
      // 정확한 노드명을 확인해서 고칠 것.
      int status_lo = MV_CC_SetFloatValue(camera_handle_, "AutoGainLowerLimit", 0.0f);
      int status_hi = MV_CC_SetFloatValue(
        camera_handle_, "AutoGainUpperLimit", static_cast<float>(gain_auto_max_db_));
      if (status_lo != MV_OK || status_hi != MV_OK) {
        RCLCPP_WARN(this->get_logger(),
          "Failed to set AutoGainLowerLimit/UpperLimit (status=0x%x/0x%x) — node name may not "
          "match this camera's GenICam map, verify via MVS Feature Tree",
          status_lo, status_hi);
      }
      RCLCPP_INFO(this->get_logger(), "Auto gain: ON, range=[0, %.1f] dB", gain_auto_max_db_);
    } else {
      MV_CC_SetEnumValue(camera_handle_, "GainAuto", 0);  // Off
      MV_CC_SetFloatValue(camera_handle_, "Gain", static_cast<float>(gain_));
      RCLCPP_INFO(this->get_logger(), "Manual gain: %.1f dB", gain_);
    }
  }

  // 2026-07-29: nTriggerIndex는 이 카메라(MV-CS016-10UC USB3)에서 항상 0으로 고정되어
  // 실사용 불가로 확인됨 (실기기 로그로 검증). 대신 nDevTimeStampHigh/Low(카메라 자체
  // 자유구동 하드웨어 클럭)는 프레임마다 정상적으로 증가하는 것을 로그로 확인했다.
  //
  // 처음엔 시작 후 고정 3초 구간만 보고 회귀를 얼려서(freeze) 썼는데, 실기기 이동
  // 테스트(bag: trigger_test__2_moving)에서 카메라-라이다 오프셋이 27초 동안
  // -12ms → -16ms로 선형 드리프트하는 것이 확인됨 — 3초짜리 창만으로는 카메라
  // 클럭의 실제 틱 주파수를 ~150ppm 정도의 오차로만 추정하게 되고, 이 오차가 세션이
  // 길어질수록 그대로 누적된다. 그래서 한 번 얼리는 대신, 최근
  // dev_clock_window_sec_ 구간의 슬라이딩 윈도우로 dev_clock_recalib_interval_sec_
  // 마다 회귀를 반복 갱신한다 — 오차가 세션 길이에 비례해 누적되지 않고 창 하나의
  // 오차 수준으로 항상 bounded 된다.
  void recalibrateDevClock()
  {
    double sum_x = 0.0, sum_y = 0.0;
    for (const auto & s : dev_clock_samples_) {
      sum_x += s.first;
      sum_y += s.second;
    }
    double n = static_cast<double>(dev_clock_samples_.size());
    double mean_x = sum_x / n;
    double mean_y = sum_y / n;

    double num = 0.0, den = 0.0;
    for (const auto & s : dev_clock_samples_) {
      double dx = s.first - mean_x;
      num += dx * (s.second - mean_y);
      den += dx * dx;
    }

    double slope_ns_per_tick = (den > 0.0) ? (num / den) : 0.0;

    if (!(den > 0.0) || !std::isfinite(slope_ns_per_tick) || slope_ns_per_tick <= 0.0) {
      dev_clock_calib_fail_count_++;
      RCLCPP_ERROR(
        this->get_logger(),
        "Device-clock timestamp (re)calibration failed (slope=%.6f, %zu samples, fail #%d).",
        slope_ns_per_tick, dev_clock_samples_.size(), dev_clock_calib_fail_count_);
      if (dev_clock_calib_fail_count_ >= 3) {
        dev_clock_permanently_disabled_ = true;
        RCLCPP_ERROR(
          this->get_logger(),
          "Device-clock timestamp calibration failed %d times in a row — this camera/SDK does "
          "not usably populate nDevTimeStamp either. Falling back to per-frame shared-memory "
          "timestamp for the rest of this session.",
          dev_clock_calib_fail_count_);
      }
      return;
    }
    dev_clock_calib_fail_count_ = 0;

    bool first_calibration = !dev_clock_usable_;
    dev_clock_slope_ns_per_tick_ = slope_ns_per_tick;
    dev_clock_mean_ticks_ = mean_x;
    dev_clock_mean_rel_ns_ = mean_y;
    dev_clock_usable_ = true;

    if (first_calibration || enable_interval_log_) {
      RCLCPP_INFO(
        this->get_logger(),
        "Device-clock timestamp %scalibrated: %.6f ns/tick (%.3f MHz), %zu samples over %.2f s",
        first_calibration ? "" : "re",
        slope_ns_per_tick, 1000.0 / slope_ns_per_tick, dev_clock_samples_.size(),
        (dev_clock_samples_.back().second - dev_clock_samples_.front().second) / 1e9);
    }
  }

  rclcpp::Time getTimestamp(const MV_FRAME_OUT_INFO_EX & frame_info)
  {
    bool have_shared_ts = pointt_ != nullptr && pointt_ != MAP_FAILED && pointt_->low != 0;

    if (trigger_enable_ && use_trigger_timestamp_ && !dev_clock_permanently_disabled_) {
      double dev_ticks = static_cast<double>(
        (static_cast<uint64_t>(frame_info.nDevTimeStampHigh) << 32) |
        frame_info.nDevTimeStampLow);

      if (have_shared_ts) {
        if (!dev_clock_ref_set_) {
          dev_clock_ref_ns_ = pointt_->low;
          dev_clock_ref_set_ = true;
        }
        double rel_ns = static_cast<double>(pointt_->low - dev_clock_ref_ns_);
        dev_clock_samples_.emplace_back(dev_ticks, rel_ns);

        // 창 크기 판정을 먼저 하고, 실제로 사용한 뒤에 오래된 샘플을 잘라낸다.
        // (이전 버그: pruning을 먼저 하면 창이 window_sec를 절대 넘을 수 없어서
        // "10초 이상 쌓였는가" 조건이 영원히 참이 될 수 없었다 — 2026-07-29
        // [DEVCLOCK-HB] 로그로 window_span_sec이 9.9~10.0에서 멈춰있는 것으로 확인.)
        double window_span_sec =
          (dev_clock_samples_.back().second - dev_clock_samples_.front().second) / 1e9;
        bool warmed_up = window_span_sec >= dev_clock_window_sec_ &&
          dev_clock_samples_.size() >= 10;
        bool due = (rel_ns - dev_clock_last_recalib_rel_ns_) >=
          dev_clock_recalib_interval_sec_ * 1e9;

        if (++dev_clock_heartbeat_count_ % 50 == 0) {
          RCLCPP_INFO(
            this->get_logger(),
            "[DEVCLOCK-HB] rel_ns=%.0f window_span_sec=%.3f samples=%zu usable=%d "
            "permanently_disabled=%d",
            rel_ns, window_span_sec, dev_clock_samples_.size(),
            dev_clock_usable_, dev_clock_permanently_disabled_);
        }

        if (warmed_up && (!dev_clock_usable_ || due)) {
          recalibrateDevClock();
          dev_clock_last_recalib_rel_ns_ = rel_ns;
        }

        while (!dev_clock_samples_.empty() &&
          (rel_ns - dev_clock_samples_.front().second) > dev_clock_window_sec_ * 1e9)
        {
          dev_clock_samples_.pop_front();
        }
      }

      if (dev_clock_usable_) {
        double rel_ns = dev_clock_mean_rel_ns_ +
          (dev_ticks - dev_clock_mean_ticks_) * dev_clock_slope_ns_per_tick_;
        int64_t final_ns = dev_clock_ref_ns_ + static_cast<int64_t>(std::llround(rel_ns));
        return rclcpp::Time(final_ns);
      }
    }

    if (enable_interval_log_) {
      RCLCPP_INFO(
        this->get_logger(),
        "[TS-DEBUG] nFrameNum=%u nTriggerIndex=%u nDevTimeStampHigh=%u nDevTimeStampLow=%u "
        "nHostTimeStamp=%ld",
        frame_info.nFrameNum, frame_info.nTriggerIndex, frame_info.nDevTimeStampHigh,
        frame_info.nDevTimeStampLow, frame_info.nHostTimeStamp);
    }

    if (have_shared_ts) {
      return rclcpp::Time(pointt_->low);
    }
    return this->now();
  }

  // ROI 제한 퍼센타일 측광으로 노출/게인을 계산해 직접 적용한다 (설계노트 4.2절 방식).
  // 밝게 해야 할 때는 노출을 exposure_auto_max_까지 먼저 늘리고, 그래도 모자라면 게인을
  // 쓴다. 어둡게 해야 할 때는 반대로 게인을 먼저 줄이고, 그래도 남으면 노출을 줄인다
  // (게인이 노이즈 비용이 있으므로 항상 마지막 수단으로 남겨두는 우선순위).
  void runSoftwareAeStep()
  {
    const int width = static_cast<int>(image_msg_.width);
    const int height = static_cast<int>(image_msg_.height);
    const double top_ratio = std::min(0.95, std::max(0.0, ae_roi_top_ratio_));
    const int row_start = static_cast<int>(height * top_ratio);
    if (row_start >= height || width <= 0) {
      return;
    }

    constexpr int kStride = 4;  // 매 프레임 전체 픽셀을 다 볼 필요 없음 — 서브샘플링
    std::vector<uint8_t> luma;
    luma.reserve((static_cast<size_t>(width) / kStride + 1) *
      (static_cast<size_t>(height - row_start) / kStride + 1));
    const uint8_t * data = image_msg_.data.data();
    const size_t step = image_msg_.step;  // width * 3 (rgb8)
    for (int y = row_start; y < height; y += kStride) {
      const uint8_t * row = data + static_cast<size_t>(y) * step;
      for (int x = 0; x < width; x += kStride) {
        const uint8_t * px = row + static_cast<size_t>(x) * 3;
        luma.push_back(static_cast<uint8_t>(
          (static_cast<int>(px[0]) + px[1] + px[2]) / 3));
      }
    }
    if (luma.empty()) {
      return;
    }
    std::sort(luma.begin(), luma.end());
    auto percentile_dn = [&luma](double p) {
      double clamped = std::min(100.0, std::max(0.0, p));
      size_t idx = static_cast<size_t>(clamped / 100.0 * static_cast<double>(luma.size() - 1));
      return static_cast<double>(luma[idx]);
    };

    const double p_target = percentile_dn(ae_target_percentile_);
    const double p_sat = percentile_dn(ae_saturation_percentile_);

    double delta_stop = std::log2(static_cast<double>(ae_target_dn_) / std::max(p_target, 1.0));
    const double delta_stop_sat_limit =
      std::log2(static_cast<double>(ae_saturation_dn_) / std::max(p_sat, 1.0));
    delta_stop = std::min(delta_stop, delta_stop_sat_limit);  // 포화 제약이 우선
    delta_stop *= ae_step_gain_;

    double remaining = delta_stop;
    if (remaining > 0.0 && exposure_auto_) {
      const double t_stop_avail = std::log2(exposure_auto_max_ / soft_ae_exposure_us_);
      const double t_stop_used = std::min(remaining, std::max(0.0, t_stop_avail));
      soft_ae_exposure_us_ *= std::pow(2.0, t_stop_used);
      remaining -= t_stop_used;
      if (gain_auto_ && remaining > 0.0) {
        const double g_stop_avail = (gain_auto_max_db_ - soft_ae_gain_db_) / 6.02;
        const double g_stop_used = std::min(remaining, std::max(0.0, g_stop_avail));
        soft_ae_gain_db_ += g_stop_used * 6.02;
      }
    } else if (remaining < 0.0) {
      double need = -remaining;
      if (gain_auto_) {
        const double g_stop_avail = soft_ae_gain_db_ / 6.02;
        const double g_stop_used = std::min(need, std::max(0.0, g_stop_avail));
        soft_ae_gain_db_ -= g_stop_used * 6.02;
        need -= g_stop_used;
      }
      if (need > 0.0 && exposure_auto_) {
        const double t_stop_avail = std::log2(soft_ae_exposure_us_ / exposure_auto_min_);
        const double t_stop_used = std::min(need, std::max(0.0, t_stop_avail));
        soft_ae_exposure_us_ /= std::pow(2.0, t_stop_used);
      }
    }

    soft_ae_exposure_us_ =
      std::min(exposure_auto_max_, std::max(exposure_auto_min_, soft_ae_exposure_us_));
    soft_ae_gain_db_ = std::min(gain_auto_max_db_, std::max(0.0, soft_ae_gain_db_));

    MV_CC_SetFloatValue(camera_handle_, "ExposureTime", static_cast<float>(soft_ae_exposure_us_));
    MV_CC_SetFloatValue(camera_handle_, "Gain", static_cast<float>(soft_ae_gain_db_));
  }

  // 실제 적용된 노출/게인(chunk 없이도 SDK가 매 프레임 넘겨주는 값)을 브로드캐스트한다.
  // 온보드 auto / 소프트웨어 AE / 수동 고정 어느 모드든 상관없이 "진짜 적용값"을 그대로 보냄.
  void publishAeState(const MV_FRAME_OUT_INFO_EX & frame_info)
  {
    std_msgs::msg::Float64 exp_msg;
    exp_msg.data = static_cast<double>(frame_info.fExposureTime);
    ae_exposure_pub_->publish(exp_msg);

    std_msgs::msg::Float64 gain_msg;
    gain_msg.data = static_cast<double>(frame_info.fGain);
    ae_gain_pub_->publish(gain_msg);
  }

  void captureLoop()
  {
    MV_FRAME_OUT out_frame;
    RCLCPP_INFO(this->get_logger(), "Publishing image!");

    image_msg_.header.frame_id = frame_id_;
    image_msg_.encoding = "rgb8";

    // [DEBUG] 타임스탬프 간격 측정용 임시 변수
    rclcpp::Time last_stamp{0, 0, RCL_ROS_TIME};
    double interval_min_ms = 1e9, interval_max_ms = 0.0, interval_sum_ms = 0.0;
    int interval_count = 0;
    static constexpr int kLogEveryN = 10;

    while (rclcpp::ok()) {
      n_ret_ = MV_CC_GetImageBuffer(camera_handle_, &out_frame, 1000);
      if (MV_OK == n_ret_) {
        convert_param_.pDstBuffer = image_msg_.data.data();
        convert_param_.nDstBufferSize = image_msg_.data.size();
        convert_param_.pSrcData = out_frame.pBufAddr;
        convert_param_.nSrcDataLen = out_frame.stFrameInfo.nFrameLen;
        convert_param_.enSrcPixelType = out_frame.stFrameInfo.enPixelType;

        MV_CC_ConvertPixelType(camera_handle_, &convert_param_);

        image_msg_.header.stamp = getTimestamp(out_frame.stFrameInfo);
        image_msg_.height = out_frame.stFrameInfo.nHeight;
        image_msg_.width = out_frame.stFrameInfo.nWidth;
        image_msg_.step = out_frame.stFrameInfo.nWidth * 3;
        image_msg_.data.resize(image_msg_.width * image_msg_.height * 3);

        if (enable_interval_log_) {
          rclcpp::Time cur_stamp = image_msg_.header.stamp;
          if (last_stamp.nanoseconds() != 0) {
            double interval_ms = (cur_stamp - last_stamp).seconds() * 1000.0;
            if (interval_ms > 0.0) {
              interval_min_ms = std::min(interval_min_ms, interval_ms);
              interval_max_ms = std::max(interval_max_ms, interval_ms);
              interval_sum_ms += interval_ms;
              interval_count++;
            }
            RCLCPP_INFO(this->get_logger(),
              "[TS] interval=%.3f ms", interval_ms);
            if (interval_count > 0 && interval_count % kLogEveryN == 0) {
              RCLCPP_INFO(this->get_logger(),
                "[TS] last %d frames: avg=%.3f ms  min=%.3f ms  max=%.3f ms  jitter=%.3f ms",
                kLogEveryN,
                interval_sum_ms / interval_count,
                interval_min_ms, interval_max_ms,
                interval_max_ms - interval_min_ms);
              interval_min_ms = 1e9; interval_max_ms = 0.0;
              interval_sum_ms = 0.0; interval_count = 0;
            }
          }
          last_stamp = cur_stamp;
        }

        camera_info_msg_.header = image_msg_.header;
        camera_pub_.publish(image_msg_, camera_info_msg_);

        if (sync_role_ != "slave" && use_software_ae_ && (exposure_auto_ || gain_auto_)) {
          runSoftwareAeStep();
        }
        if (sync_role_ == "master") {
          publishAeState(out_frame.stFrameInfo);
        }

        MV_CC_FreeImageBuffer(camera_handle_, &out_frame);

        static auto last_log_time = std::chrono::steady_clock::now();
        auto now = std::chrono::steady_clock::now();
        if (std::chrono::duration_cast<std::chrono::seconds>(now - last_log_time).count() >= 3) {
          MVCC_FLOATVALUE f_value;
          MV_CC_GetFloatValue(camera_handle_, "ResultingFrameRate", &f_value);
          RCLCPP_DEBUG(this->get_logger(), "ResultingFrameRate: %f Hz", f_value.fCurValue);

          if (balance_white_auto_) {
            // Continuous AWB로 수렴 중인 R/G/B 값을 주기적으로 노출 — 여러 카메라의 색을
            // 맞추려면 이 값을 읽어서 balance_white_auto=false + 동일 값으로 고정시키면 됨.
            MVCC_INTVALUE wb_r, wb_g, wb_b;
            MV_CC_GetBalanceRatioRed(camera_handle_, &wb_r);
            MV_CC_GetBalanceRatioGreen(camera_handle_, &wb_g);
            MV_CC_GetBalanceRatioBlue(camera_handle_, &wb_b);
            RCLCPP_INFO(
              this->get_logger(), "White balance (auto, converging): R=%u G=%u B=%u",
              wb_r.nCurValue, wb_g.nCurValue, wb_b.nCurValue);
          }
          last_log_time = now;
        }

      } else {
        RCLCPP_WARN(this->get_logger(), "Get buffer failed! nRet: [%x]", n_ret_);
        MV_CC_StopGrabbing(camera_handle_);
        MV_CC_StartGrabbing(camera_handle_);
        fail_count_++;
      }

      if (fail_count_ > 5) {
        RCLCPP_FATAL(this->get_logger(), "Camera failed!");
        rclcpp::shutdown();
      }
    }
  }

  rcl_interfaces::msg::SetParametersResult dynamicParametersCallback(
    const std::vector<rclcpp::Parameter> & parameters)
  {
    rcl_interfaces::msg::SetParametersResult result;
    result.successful = true;

    for (const auto & param : parameters) {
      const auto & type = param.get_type();
      const auto & name = param.get_name();
      int status = MV_OK;

      if (type == rclcpp::ParameterType::PARAMETER_BOOL) {
        if (name == "enable_interval_log") {
          enable_interval_log_ = param.as_bool();
        } else if (name == "exposure_auto") {
          exposure_auto_ = param.as_bool();
          applyExposureMode();
        } else if (name == "gain_auto") {
          gain_auto_ = param.as_bool();
          applyGainMode();
        } else if (name == "use_software_ae") {
          use_software_ae_ = param.as_bool();
          if (use_software_ae_) {
            soft_ae_exposure_us_ = static_cast<double>(exposure_time_);
            soft_ae_gain_db_ = gain_;
          }
          applyExposureMode();
          applyGainMode();
        } else {
          result.successful = false;
          result.reason = "Unknown parameter: " + name;
          continue;
        }
      } else if (type == rclcpp::ParameterType::PARAMETER_DOUBLE) {
        if (name == "gain") {
          gain_ = param.as_double();
          if (!gain_auto_ && !use_software_ae_) {
            status = MV_CC_SetFloatValue(camera_handle_, "Gain", static_cast<float>(gain_));
          }
        } else if (name == "exposure_auto_min") {
          exposure_auto_min_ = param.as_double();
          if (exposure_auto_) {
            status = MV_CC_SetIntValue(camera_handle_, "AutoExposureTimeLowerLimit",
              static_cast<unsigned int>(exposure_auto_min_));
          }
        } else if (name == "exposure_auto_max") {
          exposure_auto_max_ = param.as_double();
          if (exposure_auto_) {
            status = MV_CC_SetIntValue(camera_handle_, "AutoExposureTimeUpperLimit",
              static_cast<unsigned int>(exposure_auto_max_));
          }
        } else {
          result.successful = false;
          result.reason = "Unknown parameter: " + name;
          continue;
        }
      } else if (type == rclcpp::ParameterType::PARAMETER_INTEGER) {
        if (name == "exposure_time") {
          exposure_time_ = static_cast<int>(param.as_int());
          if (!exposure_auto_) {
            status = MV_CC_SetFloatValue(camera_handle_, "ExposureTime",
              static_cast<float>(exposure_time_));
          }
        } else if (name == "exposure_auto_target_brightness") {
          exposure_auto_target_brightness_ = static_cast<int>(param.as_int());
          if (exposure_auto_) {
            status = MV_CC_SetIntValue(camera_handle_, "AutoTargetBrightness",
              static_cast<unsigned int>(exposure_auto_target_brightness_));
          }
        } else {
          result.successful = false;
          result.reason = "Unknown parameter: " + name;
          continue;
        }
      } else {
        result.successful = false;
        result.reason = "Unsupported parameter type for: " + name;
        continue;
      }

      if (status != MV_OK) {
        result.successful = false;
        result.reason = "Failed to set " + name + ", status = " + std::to_string(status);
      }
    }

    return result;
  }

  // Camera
  void * camera_handle_ = nullptr;
  int n_ret_ = MV_OK;
  MV_IMAGE_BASIC_INFO img_info_;
  MV_CC_PIXEL_CONVERT_PARAM convert_param_;

  // ROS
  sensor_msgs::msg::Image image_msg_;
  sensor_msgs::msg::CameraInfo camera_info_msg_;
  image_transport::CameraPublisher camera_pub_;
  std::unique_ptr<camera_info_manager::CameraInfoManager> camera_info_manager_;
  rclcpp::node_interfaces::OnSetParametersCallbackHandle::SharedPtr params_callback_handle_;

  std::string camera_name_;
  std::string frame_id_;
  std::string camera_topic_;

  std::thread capture_thread_;
  int fail_count_ = 0;

  // Trigger & shared memory
  bool trigger_enable_ = false;
  bool use_trigger_timestamp_ = false;
  bool enable_interval_log_ = false;

  // Device-clock-based timestamp (see recalibrateDevClock() for design notes).
  // Shared-memory nanosecond values (~1.8e18, Unix epoch) exceed double's 2^53 exact-
  // integer range, so calibration samples store shared_mem_ns relative to
  // dev_clock_ref_ns_ (the first sample ever seen, kept as an exact int64_t) rather
  // than the raw epoch value.
  double dev_clock_window_sec_ = 10.0;
  double dev_clock_recalib_interval_sec_ = 2.0;
  bool dev_clock_usable_ = false;
  bool dev_clock_ref_set_ = false;
  bool dev_clock_permanently_disabled_ = false;
  int dev_clock_calib_fail_count_ = 0;
  int64_t dev_clock_ref_ns_ = 0;
  double dev_clock_last_recalib_rel_ns_ = -1e30;
  int dev_clock_heartbeat_count_ = 0;
  std::deque<std::pair<double, double>> dev_clock_samples_;  // (dev_ticks, rel_shared_mem_ns)
  double dev_clock_slope_ns_per_tick_ = 0.0;
  double dev_clock_mean_ticks_ = 0.0;
  double dev_clock_mean_rel_ns_ = 0.0;

  // Exposure
  bool exposure_auto_ = false;
  int exposure_auto_target_brightness_ = 128;
  double exposure_auto_min_ = 100.0;
  double exposure_auto_max_ = 10000.0;
  int exposure_time_ = 5000;

  // Gain
  bool gain_auto_ = false;
  double gain_ = 0.0;
  double gain_auto_max_db_ = 12.0;

  // Software AE/AG — ROI-restricted percentile metering (runSoftwareAeStep() 참조)
  bool use_software_ae_ = false;
  double ae_roi_top_ratio_ = 0.0;
  double ae_target_percentile_ = 70.0;
  int ae_target_dn_ = 130;
  double ae_saturation_percentile_ = 98.0;
  int ae_saturation_dn_ = 245;
  double ae_step_gain_ = 0.5;
  double soft_ae_exposure_us_ = 1000.0;
  double soft_ae_gain_db_ = 0.0;

  // 노출/게인 마스터-슬레이브 동기화 (initSync() 참조)
  std::string sync_role_ = "independent";
  std::string sync_master_camera_ns_;
  rclcpp::Publisher<std_msgs::msg::Float64>::SharedPtr ae_exposure_pub_;
  rclcpp::Publisher<std_msgs::msg::Float64>::SharedPtr ae_gain_pub_;
  rclcpp::Subscription<std_msgs::msg::Float64>::SharedPtr ae_exposure_sub_;
  rclcpp::Subscription<std_msgs::msg::Float64>::SharedPtr ae_gain_sub_;

  // White balance
  bool balance_white_auto_ = false;

  std::string serial_number_;
  TimeStamp * pointt_ = nullptr;
};
}  // namespace hik_camera_ros2_driver

#include "rclcpp_components/register_node_macro.hpp"
RCLCPP_COMPONENTS_REGISTER_NODE(hik_camera_ros2_driver::HikCameraRos2DriverNode)
