{"id":2346,"date":"2026-08-05T20:01:33","date_gmt":"2026-08-05T11:01:33","guid":{"rendered":"https:\/\/rfsec.ddns.net\/db\/?p=2346"},"modified":"2026-08-06T08:47:32","modified_gmt":"2026-08-05T23:47:32","slug":"zwo-asi-acuter-yolo-%e3%83%8f%e3%82%a4%e3%83%96%e3%83%aa%e3%83%83%e3%83%89%e8%87%aa%e5%8b%95%e8%bf%bd%e5%b0%be%e3%82%b7%e3%82%b9%e3%83%86%e3%83%a0-%e9%96%8b%e7%99%ba%e8%a8%98%e9%8c%b2","status":"publish","type":"post","link":"https:\/\/rfsec.ddns.net\/db\/?p=2346","title":{"rendered":"ZWO ASI + Acuter + YOLO \u30cf\u30a4\u30d6\u30ea\u30c3\u30c9\u81ea\u52d5\u8ffd\u5c3e\u30b7\u30b9\u30c6\u30e0 \u958b\u767a\u8a18\u9332"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"718\" src=\"https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2026\/08\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2026-08-06-8.35.02-1024x718.png\" alt=\"\" class=\"wp-image-2351\" srcset=\"https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2026\/08\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2026-08-06-8.35.02-1024x718.png 1024w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2026\/08\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2026-08-06-8.35.02-300x210.png 300w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2026\/08\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2026-08-06-8.35.02-768x538.png 768w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2026\/08\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2026-08-06-8.35.02-1536x1076.png 1536w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2026\/08\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2026-08-06-8.35.02-2048x1435.png 2048w, https:\/\/rfsec.ddns.net\/db\/wp-content\/uploads\/2026\/08\/\u30b9\u30af\u30ea\u30fc\u30f3\u30b7\u30e7\u30c3\u30c8-2026-08-06-8.35.02-624x437.png 624w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 id=\"0-1-%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E6%A6%82%E8%A6%81\" class=\"wp-block-heading\">1. \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u6982\u8981<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u672c\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306f\u3001ZWO ASI\u30ab\u30e1\u30e9\u3001Astromechanics EF\u30ec\u30f3\u30ba\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u3001\u304a\u3088\u3073 Acuter Traverse \u30de\u30a6\u30f3\u30c8\u3092\u7d71\u5408\u5236\u5fa1\u3057\u3001AI\u3092\u7528\u3044\u305f\u9ad8\u5ea6\u306a\u81ea\u52d5\u8ffd\u5c3e\u3068\u81ea\u52d5\u9332\u753b\u3092\u884c\u3046\u30b7\u30b9\u30c6\u30e0\u3067\u3042\u308b\u3002 \u9060\u65b9\u306e\u5fae\u5c0f\u306a\u30bf\u30fc\u30b2\u30c3\u30c8\u3092OpenCV\u306e\u52d5\u4f53\u691c\u77e5\u3067\u6355\u6349\u3057\u3001\u63a5\u8fd1\u3057\u305f\u969b\u306bYOLOv8\u306e\u7269\u4f53\u8a8d\u8b58\u3078\u30b7\u30fc\u30e0\u30ec\u30b9\u306b\u79fb\u884c\u3059\u308b\u300c\u30cf\u30a4\u30d6\u30ea\u30c3\u30c9\u30fb\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u300d\u3092\u5b9f\u73fe\u3057\u3066\u3044\u308b\u3002<\/p>\n\n\n\n<h2 id=\"1-2-%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E6%A7%8B%E6%88%90%EF%BC%883%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E6%A7%8B%E6%88%90%EF%BC%89\" class=\"wp-block-heading\">2. \u30b7\u30b9\u30c6\u30e0\u69cb\u6210\uff083\u30d5\u30a1\u30a4\u30eb\u69cb\u6210\uff09<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u8907\u96d1\u5316\u3092\u907f\u3051\u3001\u4fdd\u5b88\u6027\u3092\u9ad8\u3081\u308b\u305f\u3081\u306b\u4ee5\u4e0b\u306e3\u30d5\u30a1\u30a4\u30eb\u306b\u5206\u5272\u3057\u3066\u3044\u308b\u3002<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>device_controllers.py<\/code><\/strong>: \u30cf\u30fc\u30c9\u30a6\u30a7\u30a2\u5236\u5fa1\u5c64\uff08Acuter\u30de\u30a6\u30f3\u30c8\u306e\u30b7\u30ea\u30a2\u30eb\u901a\u4fe1\u3001EF\u30ec\u30f3\u30ba\u306e\u30d5\u30a9\u30fc\u30ab\u30b9\/\u7d5e\u308a\u5236\u5fa1\uff09\u3002<\/li>\n\n\n\n<li><strong><code>camera_yolo.py<\/code><\/strong>: \u753b\u50cf\u51e6\u7406\u5c64\uff08ZWO ASI\u304b\u3089\u306e\u30d5\u30ec\u30fc\u30e0\u53d6\u5f97\u3001YOLOv8\u306b\u3088\u308b\u63a8\u8ad6\u3001OpenCV\u306b\u3088\u308b\u52d5\u4f53\u691c\u77e5\u3001\u52d5\u753b\u4fdd\u5b58\u30ef\u30fc\u30ab\u30fc\uff09\u3002<\/li>\n\n\n\n<li><strong><code>main.py<\/code><\/strong>: UI\u304a\u3088\u3073\u7d71\u5408\u5236\u5fa1\u5c64\uff08PyQt6\u306b\u3088\u308bGUI\u3001PID\u5236\u5fa1\uff08P\u5236\u5fa1\uff09\u306b\u3088\u308b\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u30a2\u30eb\u30b4\u30ea\u30ba\u30e0\u3001\u63cf\u753b\u30aa\u30fc\u30d0\u30fc\u30ec\u30a4\uff09\u3002<\/li>\n<\/ul>\n\n\n\n<h2 id=\"2-3-%E9%96%8B%E7%99%BA%E3%81%AE%E7%B5%8C%E7%B7%AF%E3%81%A8%E4%B8%BB%E8%A6%81%E3%81%AA%E3%83%88%E3%83%A9%E3%83%96%E3%83%AB%E3%82%B7%E3%83%A5%E3%83%BC%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0\" class=\"wp-block-heading\">3. \u958b\u767a\u306e\u7d4c\u7def\u3068\u4e3b\u8981\u306a\u30c8\u30e9\u30d6\u30eb\u30b7\u30e5\u30fc\u30c6\u30a3\u30f3\u30b0<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u958b\u767a\u904e\u7a0b\u306b\u304a\u3044\u3066\u3001\u4ee5\u4e0b\u306e\u6280\u8853\u7684\u8ab2\u984c\u3092\u89e3\u6c7a\u3057\u30b7\u30b9\u30c6\u30e0\u3092\u5b89\u5b9a\u5316\u3055\u305b\u305f\u3002<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>\u540d\u524d\u7a7a\u9593\u306e\u885d\u7a81\u89e3\u6c7a (v2.33)<\/strong>\n<ul class=\"wp-block-list\">\n<li>[\u8ab2\u984c] \u6a19\u6e96\u30e9\u30a4\u30d6\u30e9\u30ea\u306e <code class=\"\">hardware<\/code> \u3068\u30d5\u30a1\u30a4\u30eb\u540d\u304c\u885d\u7a81\u3057\u30a4\u30f3\u30dd\u30fc\u30c8\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3002<\/li>\n\n\n\n<li>[\u89e3\u6c7a] \u30d5\u30a1\u30a4\u30eb\u540d\u3092 <code class=\"\">device_controllers.py<\/code> \u306b\u5909\u66f4\u3057\u3066\u5206\u96e2\u3002<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>\u30de\u30a6\u30f3\u30c8\u306e\u30aa\u30fc\u30d0\u30fc\u30b7\u30e5\u30fc\u30c8\u30fb\u884c\u304d\u904e\u304e\u554f\u984c\u306e\u89e3\u6c7a (v2.34 &#8211; v2.35)<\/strong>\n<ul class=\"wp-block-list\">\n<li>[\u8ab2\u984c] \u8ffd\u5c3e\u6642\u306b\u30bf\u30fc\u30b2\u30c3\u30c8\u304c\u4e2d\u5fc3\uff08\u30c7\u30c3\u30c9\u30d0\u30f3\u30c9\uff09\u306b\u5165\u3063\u3066\u3082\u30de\u30a6\u30f3\u30c8\u304c\u6b62\u307e\u3089\u305a\u884c\u304d\u904e\u304e\u308b\u3002<\/li>\n\n\n\n<li>[\u89e3\u6c7a] \u6bce\u79d220\u56de\u306e\u30b3\u30de\u30f3\u30c9\u9001\u4fe1\u306b\u3088\u308b\u30b7\u30ea\u30a2\u30eb\u30d0\u30c3\u30d5\u30a1\u306e\u6e0b\u6ede\u304c\u539f\u56e0\u3068\u5224\u660e\u3002\u901a\u4fe1\u983b\u5ea6\u3092\u6bce\u79d210\u56de\u306b\u843d\u3068\u3057\u3001\u4e0d\u8981\u306a\u8aad\u307f\u8fd4\u3057\uff08<code class=\"\">:J<\/code>\uff09\u3092\u524a\u9664\u3002\u307e\u305f\u3001\u30d6\u30ec\u30fc\u30ad\u6642\u306b\u306f\u30d0\u30c3\u30d5\u30a1\u3092\u5f37\u5236\u7834\u68c4\u3057\u3001Acuter\u306e\u77ac\u6642\u505c\u6b62\u30b3\u30de\u30f3\u30c9\uff08<code class=\"\">:L<\/code>\uff09\u3092\u53e9\u304d\u8fbc\u3080\u8a2d\u8a08\u306b\u5909\u66f4\u3002<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Auto Engage\uff08\u81ea\u52d5\u691c\u77e5\uff09\u3068\u9332\u753b\u306e\u7d30\u5207\u308c\u554f\u984c (v2.36)<\/strong>\n<ul class=\"wp-block-list\">\n<li>[\u8ab2\u984c] \u5bfe\u8c61\u3092\u5c11\u3057\u3067\u3082\u898b\u5931\u3046\u3068\u5225\u30bf\u30fc\u30b2\u30c3\u30c8\u3068\u8a8d\u8b58\u3055\u308c\u3001\u52d5\u753b\u304c\u7d30\u5207\u308c\u306b\u306a\u308b\u3002\u5bfe\u8c61\u5916\u3092\u30af\u30ea\u30c3\u30af\u3057\u3066\u3082Auto Engage\u304c\u5373\u5ea7\u306b\u518d\u30ed\u30c3\u30af\u3057\u3066\u3057\u307e\u3046\u3002<\/li>\n\n\n\n<li>[\u89e3\u6c7a] \u30ed\u30b9\u30c8\u7336\u4e88\u671f\u9593\u3092\u5927\u5e45\u306b\u5ef6\u9577\uff08\u6700\u5927150\u30d5\u30ec\u30fc\u30e0\/\u7d045\u79d2\uff09\u3002\u8ddd\u96e2\u5236\u9650\u3092\u64a4\u5ec3\u3057\u3001\u753b\u9762\u5185\u306b\u540c\u3058\u30e9\u30d9\u30eb\u304c\u518d\u51fa\u73fe\u3057\u305f\u5834\u5408\u306f\u540c\u4e00\u30bf\u30fc\u30b2\u30c3\u30c8\u3068\u3057\u3066\u30ea\u30ab\u30d0\u30ea\u30fc\u3059\u308b\u51e6\u7406\u3092\u8ffd\u52a0\u3002\u7a7a\u30af\u30ea\u30c3\u30af\u6642\u306fAuto Engage\u3092\u30aa\u30d5\u306b\u3057\u3066\u5f37\u5236\u30ad\u30e3\u30f3\u30bb\u30eb\u3059\u308b\u3088\u3046\u4ed5\u69d8\u5909\u66f4\u3002<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>\u30cf\u30a4\u30d6\u30ea\u30c3\u30c9\u30fb\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u306e\u5b9f\u88c5 (v2.38)<\/strong>\n<ul class=\"wp-block-list\">\n<li>[\u8ab2\u984c] YOLO\u304c\u8a8d\u8b58\u3067\u304d\u306a\u3044\u9060\u65b9\u306e\u5c0f\u3055\u306a\u30bf\u30fc\u30b2\u30c3\u30c8\u3092\u8ffd\u5c3e\u3067\u304d\u306a\u3044\u3002<\/li>\n\n\n\n<li>[\u89e3\u6c7a] OpenCV\u306e\u8f2a\u90ed\u62bd\u51fa\u3092\u7528\u3044\u305f\u52d5\u4f53\u691c\u77e5\uff08Motion Detect\uff09\u3092\u8ffd\u52a0\u3002\u52d5\u4f53\u3092\u8ffd\u5c3e\u4e2d\u306bYOLO\u304c\u540c\u3058\u5ea7\u6a19\u3067\u7269\u4f53\u3092\u8a8d\u8b58\u3057\u305f\u5834\u5408\u3001YOLO\u306e\u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\uff08ID\u4fdd\u6301\uff09\u306b\u81ea\u52d5\u3067\u30a2\u30c3\u30d7\u30b0\u30ec\u30fc\u30c9\u3059\u308b\u9023\u643a\u6a5f\u69cb\u3092\u5b9f\u88c5\u3002<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Slew\u30e2\u30fc\u30c9\uff08\u624b\u52d5\/\u8ffd\u5c3e\u56de\u8ee2\uff09\u306e\u52d5\u4f5c\u4e0d\u80fd\u554f\u984c (v2.39)<\/strong>\n<ul class=\"wp-block-list\">\n<li>[\u8ab2\u984c] \u77e2\u5370\u30ad\u30fc\u3084\u8ffd\u5c3e\u6642\u306b\u30de\u30a6\u30f3\u30c8\u304c\u56de\u8ee2\u3057\u306a\u3044\u3002<\/li>\n\n\n\n<li>[\u89e3\u6c7a] Sky-Watcher\uff08Acuter\uff09\u306e\u901a\u4fe1\u30d7\u30ed\u30c8\u30b3\u30eb\u4ed5\u69d8\u306b\u57fa\u3065\u304d\u3001Slew\u30e2\u30fc\u30c9\u3067\u3082\u5fc5\u9808\u3068\u306a\u308b\u5b9f\u884c\u30b3\u30de\u30f3\u30c9\uff08<code class=\"\">:J<\/code>\uff09\u3092\u6b63\u3057\u304f\u518d\u5b9f\u88c5\u3002\u307e\u305f\u3001\u9006\u56de\u8ee2\u3092\u6307\u793a\u3055\u308c\u305f\u969b\u306f\u30e2\u30fc\u30bf\u30fc\u4fdd\u8b77\u306e\u305f\u3081\u5fc5\u305a\u505c\u6b62\u30b3\u30de\u30f3\u30c9\uff08<code class=\"\">:L<\/code>\uff09\u3092\u631f\u3080\u51e6\u7406\u3092\u8ffd\u52a0\u3002<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n<div class=\"wp-block-ub-content-toggle wp-block-ub-content-toggle-block\" id=\"ub-content-toggle-block-40c248f0-ac6a-4b3b-a415-920f81499c5e\" data-mobilecollapse=\"false\" data-desktopcollapse=\"false\" data-preventcollapse=\"false\" data-showonlyone=\"false\">\n<div class=\"wp-block-ub-content-toggle-accordion\" style=\"border-color: #f1f1f1; border-top-left-radius: 8px; border-top-right-radius: 8px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; \" id=\"ub-content-toggle-panel-block-\">\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-title-wrap\" style=\"background-color: #f1f1f1;\" aria-controls=\"ub-content-toggle-panel-0-40c248f0-ac6a-4b3b-a415-920f81499c5e\" tabindex=\"0\">\n\t\t\t<p class=\"wp-block-ub-content-toggle-accordion-title ub-content-toggle-title-40c248f0-ac6a-4b3b-a415-920f81499c5e\" style=\"color: #000000; \"><strong>\/Users\/mars\/acuter\/main.py<\/strong><\/p>\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-toggle-wrap right\" style=\"color: #000000;\"><span class=\"wp-block-ub-content-toggle-accordion-state-indicator wp-block-ub-chevron-down open\"><\/span><\/div>\n\t\t<\/div>\n\t\t\t<div role=\"region\" aria-expanded=\"true\" class=\"wp-block-ub-content-toggle-accordion-content-wrap\" id=\"ub-content-toggle-panel-0-40c248f0-ac6a-4b3b-a415-920f81499c5e\">\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-plain\"><code># CodeName: ZWO_ASI_Acuter_EF_YOLOv8_ContinuousTrack_AutoEngage_UI_Clean\n# Version: 2.39.0\n\nimport sys\nimport os\nimport time\nimport cv2\nimport numpy as np\nimport zwoasi as asi\n\nfrom PyQt6.QtCore import Qt, QTimer\nfrom PyQt6.QtGui import QImage, QPixmap\nfrom PyQt6.QtWidgets import (\n    QApplication, QCheckBox, QFormLayout, QGroupBox, QHBoxLayout, QLabel,\n    QMainWindow, QScrollArea, QSpinBox, QDoubleSpinBox, QStatusBar, \n    QVBoxLayout, QWidget, QLineEdit, QPushButton, QMessageBox, QComboBox, \n    QGridLayout, QTabWidget\n)\n\nfrom device_controllers import AcuterController, AstromechanicsEFController, PositionWorker, MoveWorker\nfrom camera_yolo import download_model_files, YOLO_MODEL_FILE, VideoLabel, CaptureThread, ControlRow, VideoSaveWorker\n\nclass MainWindow(QMainWindow):\n    def __init__(self, sdk_path):\n        super().__init__()\n        self.setWindowTitle('ZWO ASI Camera + EF Lens + Acuter + YOLOv8 Complete (v2.39.0)')\n        self.resize(1300, 900)\n\n        asi.init(sdk_path)\n        if asi.get_num_cameras() == 0:\n            raise RuntimeError('ZWO\u30ab\u30e1\u30e9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093')\n\n        self.camera = asi.Camera(0)\n        self.camera.set_image_type(asi.ASI_IMG_RGB24)\n        \n        cam_prop = self.camera.get_camera_property()\n        supported_bins = cam_prop.get('SupportedBins', [1])\n        self.current_bins = 2 if 2 in supported_bins else 1\n        current_w = cam_prop['MaxWidth'] \/\/ self.current_bins\n        current_h = cam_prop['MaxHeight'] \/\/ self.current_bins\n        \n        self.camera.set_roi(start_x=0, start_y=0, width=current_w, height=current_h, bins=self.current_bins)\n        self.current_start_x = 0\n        self.current_start_y = 0\n\n        self.target_id = None\n        self.last_target_pos = None\n        self.last_target_name = None\n        self.current_detections = []\n        self.last_frame_size = (current_w, current_h)\n        self.latest_frame = None\n        self.last_motion_box = None\n\n        self.pixel_size = 0.0038\n        self.next_track_time = 0.0\n        self.lost_counter = 0\n        \n        self.is_recording_event = False\n        self.video_buffer = []\n        self.home_deg = {1: None, 2: None}\n        \n        self.debug_info_dxdy = \"Target diff: N\/A\"\n        self.debug_info_azalt = \"Cmd: N\/A\"\n        self.debug_info_status = \"Status: Idle\"\n        \n        self.calib_state = 0\n        self.calib_timer = QTimer(self)\n        self.calib_timer.timeout.connect(self.calib_step)\n        self.calib_pts_prev = None\n        self.calib_gray_prev = None\n\n        self.acuter = AcuterController()\n        self.acuter_poll_timer = QTimer(self)\n        self.acuter_poll_timer.timeout.connect(self.poll_acuter_position)\n\n        self.ef_controller = AstromechanicsEFController()\n        self.capture_thread = None\n\n        self._build_ui()\n        self._update_ef_ui_state(False)\n\n        self._frame_count = 0\n        self._start_capture()\n        QTimer.singleShot(500, self.auto_connect_devices)\n\n    def auto_connect_devices(self):\n        port_ef = self.ef_port_edit.text().strip()\n        if port_ef:\n            self.ef_controller.port = port_ef\n            self.statusBar().showMessage(\"EF\u30ec\u30f3\u30ba\u81ea\u52d5\u63a5\u7d9a\u8a66\u884c\u4e2d...\")\n            try:\n                if self.ef_controller.connect():\n                    self._update_ef_ui_state(True)\n                    self.btn_ef_connect.setText(\"\u63a5\u7d9a\u6e08\")\n                    self.btn_ef_connect.setStyleSheet(\"background-color: #28a745; color: white;\")\n                    self.on_ef_refresh_position()\n            except Exception:\n                self.ef_controller.disconnect()\n\n        port_acuter = self.acuter_port_input.text().strip()\n        if port_acuter:\n            self.statusBar().showMessage(\"Acuter\u30de\u30a6\u30f3\u30c8\u81ea\u52d5\u63a5\u7d9a\u8a66\u884c\u4e2d...\")\n            try:\n                if self.acuter.connect(port_acuter):\n                    self.btn_acuter_connect.setText(\"\u5207\u65ad\")\n                    self.btn_acuter_connect.setStyleSheet(\"background-color: #28a745; color: white;\")\n                    self.lbl_acuter_status.setText(\"\u63a5\u7d9a\u6e08\u307f (GoTo\/Slew\u6709\u52b9)\")\n                    self.lbl_acuter_status.setStyleSheet(\"color: #00FF00; font-weight: bold;\")\n                    self._set_acuter_controls_state(True)\n                    self.acuter_poll_timer.start(500)\n            except Exception:\n                self.statusBar().showMessage(\"\u81ea\u52d5\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u5f8c\u3067\u624b\u52d5\u63a5\u7d9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002\")\n\n    def _start_capture(self):\n        if self.capture_thread and self.capture_thread.isRunning():\n            self.capture_thread.stop()\n        \n        self.capture_thread = CaptureThread(self.camera, YOLO_MODEL_FILE)\n        self.capture_thread.target_classes = self.get_current_target_classes()\n        self.capture_thread.detection_enabled = self.cb_detection.isChecked()\n        self.capture_thread.motion_enabled = self.cb_motion.isChecked()\n        self.capture_thread.frame_ready.connect(self._on_frame)\n        self.capture_thread.error.connect(lambda msg: self.statusBar().showMessage(f\"\u53d6\u5f97\u30a8\u30e9\u30fc: {msg}\"))\n        self.capture_thread.start()\n\n    def _build_ui(self):\n        central = QWidget()\n        self.setCentralWidget(central)\n        root = QHBoxLayout(central)\n        root.setContentsMargins(15, 15, 15, 15)\n        root.setSpacing(15)\n\n        # ========== \u5de6\u30da\u30a4\u30f3 ==========\n        left_widget = QWidget()\n        left_layout = QVBoxLayout(left_widget)\n        left_layout.setContentsMargins(0, 0, 0, 0)\n        left_layout.setSpacing(15)\n\n        self.preview_label = VideoLabel('starting...')\n        self.preview_label.setMinimumSize(640, 480)\n        self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        self.preview_label.setStyleSheet(\"background-color: #000000; color: #FFFFFF; border-radius: 8px;\")\n        self.preview_label.clicked.connect(self._on_preview_clicked)\n        self.preview_label.roi_selected.connect(self._on_roi_dragged)\n        left_layout.addWidget(self.preview_label, stretch=1)\n\n        self.ef_group = QGroupBox(\"Canon EF Lens (Astromechanics)\")\n        ef_layout = QVBoxLayout(self.ef_group)\n        \n        ef_conn_layout = QHBoxLayout()\n        self.ef_port_edit = QLineEdit(\"\/dev\/tty.usbserial-AK06UIRD\")\n        ef_conn_layout.addWidget(QLabel(\"\u30dd\u30fc\u30c8:\"))\n        ef_conn_layout.addWidget(self.ef_port_edit, stretch=1)\n        self.btn_ef_connect = QPushButton(\"\u63a5\u7d9a\")\n        self.btn_ef_connect.clicked.connect(self.on_ef_connect)\n        self.btn_ef_disconnect = QPushButton(\"\u5207\u65ad\")\n        self.btn_ef_disconnect.clicked.connect(self.on_ef_disconnect)\n        ef_conn_layout.addWidget(self.btn_ef_connect)\n        ef_conn_layout.addWidget(self.btn_ef_disconnect)\n        ef_layout.addLayout(ef_conn_layout)\n\n        ef_pos_move_row = QHBoxLayout()\n        self.lbl_ef_position = QLabel(\"\u2014\")\n        self.lbl_ef_position.setStyleSheet(\"color: white; background-color: #333; padding: 4px; border-radius: 4px;\")\n        self.btn_ef_refresh = QPushButton(\"\u66f4\u65b0\")\n        self.btn_ef_refresh.clicked.connect(self.on_ef_refresh_position)\n        self.spin_ef_target = QSpinBox()\n        self.spin_ef_target.setRange(0, 32767)\n        self.spin_ef_target.setValue(5000)\n        self.spin_ef_target.setSingleStep(100)\n        self.btn_ef_goto = QPushButton(\"\u79fb\u52d5\")\n        self.btn_ef_goto.clicked.connect(self.on_ef_goto)\n        ef_pos_move_row.addWidget(QLabel(\"\u73fe\u5728\u4f4d\u7f6e:\"))\n        ef_pos_move_row.addWidget(self.lbl_ef_position)\n        ef_pos_move_row.addWidget(self.btn_ef_refresh)\n        ef_pos_move_row.addSpacing(20)\n        ef_pos_move_row.addWidget(QLabel(\"\u76ee\u6a19\u4f4d\u7f6e:\"))\n        ef_pos_move_row.addWidget(self.spin_ef_target)\n        ef_pos_move_row.addWidget(self.btn_ef_goto)\n        ef_layout.addLayout(ef_pos_move_row)\n\n        ef_rel_ap_row = QHBoxLayout()\n        self.spin_ef_rel = QSpinBox()\n        self.spin_ef_rel.setRange(1, 5000)\n        self.spin_ef_rel.setValue(100)\n        self.spin_ef_rel.setSingleStep(50)\n        self.btn_ef_in = QPushButton(\"\u2190 IN (\u2212)\")\n        self.btn_ef_in.clicked.connect(lambda: self.on_ef_relative(-1))\n        self.btn_ef_out = QPushButton(\"OUT (+) \u2192\")\n        self.btn_ef_out.clicked.connect(lambda: self.on_ef_relative(+1))\n        self.spin_ef_aperture = QSpinBox()\n        self.spin_ef_aperture.setRange(0, 30)\n        self.spin_ef_aperture.setValue(0)\n        self.btn_ef_aperture = QPushButton(\"\u8a2d\u5b9a\")\n        self.btn_ef_aperture.clicked.connect(self.on_ef_set_aperture)\n        ef_rel_ap_row.addWidget(QLabel(\"\u76f8\u5bfe:\"))\n        ef_rel_ap_row.addWidget(self.spin_ef_rel)\n        ef_rel_ap_row.addWidget(self.btn_ef_in)\n        ef_rel_ap_row.addWidget(self.btn_ef_out)\n        ef_rel_ap_row.addSpacing(20)\n        ef_rel_ap_row.addWidget(QLabel(\"\u7d5e\u308a(0=\u958b\u653e):\"))\n        ef_rel_ap_row.addWidget(self.spin_ef_aperture)\n        ef_rel_ap_row.addWidget(self.btn_ef_aperture)\n        ef_rel_ap_row.addStretch()\n        ef_layout.addLayout(ef_rel_ap_row)\n        left_layout.addWidget(self.ef_group)\n        root.addWidget(left_widget, stretch=3)\n\n        # ========== \u53f3\u30da\u30a4\u30f3 ==========\n        right_scroll = QScrollArea()\n        right_scroll.setWidgetResizable(True)\n        right_scroll.setMinimumWidth(430)\n        right_scroll.setFrameShape(QScrollArea.Shape.NoFrame)\n\n        right_panel = QWidget()\n        right_layout = QVBoxLayout(right_panel)\n        right_layout.setContentsMargins(0, 0, 10, 0)\n        \n        self.ai_group = QGroupBox(\"AI Object Tracking\")\n        ai_layout = QVBoxLayout(self.ai_group)\n        self.cb_detection = QCheckBox(\"YOLOv8 \u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u3092\u6709\u52b9\u306b\u3059\u308b (ID\u4fdd\u6301)\")\n        self.cb_detection.setChecked(False)\n        self.cb_detection.toggled.connect(self.on_detection_toggled)\n        ai_layout.addWidget(self.cb_detection)\n\n        self.cb_motion = QCheckBox(\"\u52d5\u4f53\u691c\u77e5 (Motion Detect) [YOLO\u8a8d\u8b58\u524d\u306e\u5c0f\u76ee\u6a19\u7528]\")\n        self.cb_motion.setChecked(True)\n        self.cb_motion.setStyleSheet(\"color: #ffcc00; font-weight: bold;\")\n        self.cb_motion.toggled.connect(self.on_motion_toggled)\n        ai_layout.addWidget(self.cb_motion)\n\n        info_label = QLabel(\"\u203b\u30af\u30ea\u30c3\u30af\u3067\u300c\u5bfe\u8c61\u30ed\u30c3\u30af\u30aa\u30f3\u300d\u3001\u30c9\u30e9\u30c3\u30b0\u3067\u300cROI\u5207\u308a\u51fa\u3057\u300d\")\n        info_label.setStyleSheet(\"color: #aaaaaa; font-size: 11px;\")\n        ai_layout.addWidget(info_label)\n\n        targets_layout = QHBoxLayout()\n        self.cb_person = QCheckBox(\"\u4eba\")\n        self.cb_bicycle = QCheckBox(\"\u81ea\u8ee2\u8eca\")\n        self.cb_car = QCheckBox(\"\u8eca\")\n        self.cb_airplane = QCheckBox(\"\u822a\u7a7a\u6a5f\")\n        self.cb_bird = QCheckBox(\"\u9ce5\")\n        for cb in [self.cb_person, self.cb_bicycle, self.cb_car, self.cb_airplane, self.cb_bird]:\n            cb.setChecked(True)\n            cb.toggled.connect(self.update_target_classes)\n            targets_layout.addWidget(cb)\n        ai_layout.addLayout(targets_layout)\n        \n        self.cb_auto_engage = QCheckBox(\"\u9078\u629e\u5bfe\u8c61\u306e\u81ea\u52d5\u691c\u77e5\uff06\u9332\u753b (Auto Engage)\")\n        self.cb_auto_engage.setChecked(False)\n        self.cb_auto_engage.setStyleSheet(\"color: #5c9eff; font-weight: bold;\")\n        ai_layout.addWidget(self.cb_auto_engage)\n        right_layout.addWidget(self.ai_group)\n\n        self.acuter_group = QGroupBox(\"Acuter Traverse Control\")\n        self.acuter_layout = QVBoxLayout(self.acuter_group)\n\n        acuter_conn_layout = QHBoxLayout()\n        self.acuter_port_input = QLineEdit(\"\/dev\/cu.usbmodem4E94509B34001\")\n        self.btn_acuter_connect = QPushButton(\"\u63a5\u7d9a\")\n        self.btn_acuter_connect.clicked.connect(self.toggle_acuter_connection)\n        acuter_conn_layout.addWidget(self.acuter_port_input)\n        acuter_conn_layout.addWidget(self.btn_acuter_connect)\n        self.acuter_layout.addLayout(acuter_conn_layout)\n\n        self.lbl_acuter_status = QLabel(\"\u672a\u63a5\u7d9a\")\n        self.lbl_acuter_status.setStyleSheet(\"color: red; font-weight: bold;\")\n        self.acuter_layout.addWidget(self.lbl_acuter_status)\n        \n        self.btn_auto_track = QPushButton(\"Auto Tracking (OFF \/ \u5f85\u6a5f)\")\n        self.btn_auto_track.setCheckable(True)\n        self.btn_auto_track.setFixedHeight(60)\n        self.btn_auto_track.setStyleSheet(\"font-size: 18px; font-weight: bold; background-color: #555555; color: white;\")\n        self.btn_auto_track.toggled.connect(self.on_auto_track_toggled)\n        self.acuter_layout.addWidget(self.btn_auto_track)\n\n        pos_layout = QHBoxLayout()\n        self.lbl_acuter_az = QLabel(\"Az : --.- \u00b0\")\n        self.lbl_acuter_az.setStyleSheet(\"font-family: 'Menlo', 'Consolas', 'Courier New'; font-size: 16px; font-weight: bold; color: white; background-color: #333; padding: 4px; border-radius: 4px;\")\n        self.lbl_acuter_alt = QLabel(\"Alt: --.- \u00b0\")\n        self.lbl_acuter_alt.setStyleSheet(\"font-family: 'Menlo', 'Consolas', 'Courier New'; font-size: 16px; font-weight: bold; color: white; background-color: #333; padding: 4px; border-radius: 4px;\")\n        pos_layout.addWidget(self.lbl_acuter_az)\n        pos_layout.addWidget(self.lbl_acuter_alt)\n        self.acuter_layout.addLayout(pos_layout)\n\n        speed_form = QFormLayout()\n        self.acuter_speed_combo = QComboBox()\n        self.acuter_speed_combo.addItems([\"1.0\", \"5.0\", \"10.0\", \"15.0\"])\n        self.acuter_speed_combo.setCurrentText(\"15.0\")\n        speed_form.addRow(\"\u6700\u5927\u56de\u8ee2\u901f\u5ea6(\u5ea6\/\u79d2):\", self.acuter_speed_combo)\n        self.acuter_layout.addLayout(speed_form)\n\n        goto_layout = QHBoxLayout()\n        self.acuter_az_input = QLineEdit(\"10.0\")\n        self.acuter_az_input.setMaximumWidth(50)\n        self.btn_az_goto = QPushButton(\"Az GoTo\")\n        self.btn_az_goto.setEnabled(False)\n        self.btn_az_goto.clicked.connect(lambda: self.acuter.start_move(1, float(self.acuter_az_input.text()), exact_goto=True))\n        self.acuter_alt_input = QLineEdit(\"10.0\")\n        self.acuter_alt_input.setMaximumWidth(50)\n        self.btn_alt_goto = QPushButton(\"Alt GoTo\")\n        self.btn_alt_goto.setEnabled(False)\n        self.btn_alt_goto.clicked.connect(lambda: self.acuter.start_move(2, float(self.acuter_alt_input.text()), exact_goto=True))\n        goto_layout.addWidget(QLabel(\"Az:\"))\n        goto_layout.addWidget(self.acuter_az_input)\n        goto_layout.addWidget(QLabel(\"\u00b0\"))\n        goto_layout.addWidget(self.btn_az_goto)\n        goto_layout.addSpacing(15)\n        goto_layout.addWidget(QLabel(\"Alt:\"))\n        goto_layout.addWidget(self.acuter_alt_input)\n        goto_layout.addWidget(QLabel(\"\u00b0\"))\n        goto_layout.addWidget(self.btn_alt_goto)\n        goto_layout.addStretch()\n        self.acuter_layout.addLayout(goto_layout)\n\n        dpad_layout = QGridLayout()\n        self.btn_up = QPushButton(\"\u25b2\")\n        self.btn_left = QPushButton(\"\u25c0\")\n        self.btn_stop_center = QPushButton(\"\u25a0 STOP\")\n        self.btn_stop_center.setStyleSheet(\"background-color: #d9534f; color: white; font-weight: bold;\")\n        self.btn_right = QPushButton(\"\u25b6\")\n        self.btn_down = QPushButton(\"\u25bc\")\n\n        huge_angle = 1000.0 \n        self.btn_up.pressed.connect(lambda: self.acuter.start_move(2, -huge_angle, speed_deg_sec=float(self.acuter_speed_combo.currentText())))\n        self.btn_down.pressed.connect(lambda: self.acuter.start_move(2, huge_angle, speed_deg_sec=float(self.acuter_speed_combo.currentText())))\n        self.btn_left.pressed.connect(lambda: self.acuter.start_move(1, -huge_angle, speed_deg_sec=float(self.acuter_speed_combo.currentText())))\n        self.btn_right.pressed.connect(lambda: self.acuter.start_move(1, huge_angle, speed_deg_sec=float(self.acuter_speed_combo.currentText())))\n        \n        self.btn_up.released.connect(lambda: self.acuter.stop_axis(2))\n        self.btn_down.released.connect(lambda: self.acuter.stop_axis(2))\n        self.btn_left.released.connect(lambda: self.acuter.stop_axis(1))\n        self.btn_right.released.connect(lambda: self.acuter.stop_axis(1))\n        self.btn_stop_center.clicked.connect(self.acuter.emergency_stop)\n\n        dpad_layout.addWidget(self.btn_up, 0, 1)\n        dpad_layout.addWidget(self.btn_left, 1, 0)\n        dpad_layout.addWidget(self.btn_stop_center, 1, 1)\n        dpad_layout.addWidget(self.btn_right, 1, 2)\n        dpad_layout.addWidget(self.btn_down, 2, 1)\n        self.acuter_layout.addLayout(dpad_layout)\n        right_layout.addWidget(self.acuter_group)\n\n        # 3. ZWO ASI \u30ab\u30e1\u30e9\u8a2d\u5b9a\n        asi_group = QGroupBox('ZWO ASI &amp; Tracking Settings')\n        asi_layout = QVBoxLayout(asi_group)\n\n        self.cam_tabs = QTabWidget()\n        self.tab_main = QWidget()\n        self.tab_adv = QWidget()\n        form_main = QFormLayout(self.tab_main)\n        form_adv = QFormLayout(self.tab_adv)\n\n        # \u2605 Main\u30bf\u30d6\u306b\u306f\u30ad\u30e3\u30ea\u30d6\u30ec\u30fc\u30b7\u30e7\u30f3\u30dc\u30bf\u30f3\u306e\u307f\u3092\u914d\u7f6e\n        self.btn_auto_calib = QPushButton(\"Run Auto Calibration\")\n        self.btn_auto_calib.clicked.connect(self.start_auto_calib)\n        self.btn_auto_calib.setStyleSheet(\"background-color: #28a745; color: white; font-weight: bold; font-size: 16px; padding: 12px;\")\n        form_main.addRow(self.btn_auto_calib)\n\n        # \u2605 \u305d\u306e\u4ed6\u306e\u8a2d\u5b9a\u306f\u3059\u3079\u3066Advanced\u30bf\u30d6\u3078\u79fb\u52d5\n        track_calib_group = QGroupBox(\"Tracking Settings &amp; Axis Mapping\")\n        track_calib_layout = QFormLayout(track_calib_group)\n\n        self.focal_spin = QDoubleSpinBox()\n        self.focal_spin.setRange(1.0, 2000.0)\n        self.focal_spin.setValue(55.0) # \u521d\u671f\u5024\u309255.0\u306b\u5909\u66f4\n        self.kp_spin = QDoubleSpinBox()\n        self.kp_spin.setRange(0.01, 5.0)\n        self.kp_spin.setSingleStep(0.1)\n        self.kp_spin.setValue(0.8)\n        self.deadband_spin = QSpinBox()\n        self.deadband_spin.setRange(5, 200)\n        self.deadband_spin.setValue(40)\n\n        self.combo_x_axis = QComboBox()\n        self.combo_x_axis.addItems([\"Az (Axis 1)\", \"Alt (Axis 2)\"])\n        self.combo_x_axis.setCurrentIndex(0) \n        self.combo_y_axis = QComboBox()\n        self.combo_y_axis.addItems([\"Alt (Axis 2)\", \"Az (Axis 1)\"])\n        self.combo_y_axis.setCurrentIndex(0)\n        self.cb_inv_x = QCheckBox(\"Invert X-Axis (Reverse)\")\n        self.cb_inv_y = QCheckBox(\"Invert Y-Axis (Reverse)\")\n        self.cb_inv_y.setChecked(True)\n\n        test_move_widget = QWidget()\n        test_move_grid = QGridLayout(test_move_widget)\n        self.btn_test_alt_p = QPushButton(\"Alt +1.0\u00b0\")\n        self.btn_test_alt_m = QPushButton(\"Alt -1.0\u00b0\")\n        self.btn_test_az_m = QPushButton(\"Az -1.0\u00b0\")\n        self.btn_test_az_p = QPushButton(\"Az +1.0\u00b0\")\n        self.btn_test_alt_p.clicked.connect(lambda: self.acuter.start_move(2, 1.0, exact_goto=True, speed_deg_sec=2.0))\n        self.btn_test_alt_m.clicked.connect(lambda: self.acuter.start_move(2, -1.0, exact_goto=True, speed_deg_sec=2.0))\n        self.btn_test_az_m.clicked.connect(lambda: self.acuter.start_move(1, -1.0, exact_goto=True, speed_deg_sec=2.0))\n        self.btn_test_az_p.clicked.connect(lambda: self.acuter.start_move(1, 1.0, exact_goto=True, speed_deg_sec=2.0))\n        \n        self.calib_buttons = [self.btn_test_alt_p, self.btn_test_alt_m, self.btn_test_az_m, self.btn_test_az_p, self.btn_auto_calib]\n        for btn in self.calib_buttons: btn.setEnabled(False)\n\n        test_move_grid.addWidget(self.btn_test_alt_p, 0, 1)\n        test_move_grid.addWidget(self.btn_test_az_m, 1, 0)\n        test_move_grid.addWidget(self.btn_test_alt_m, 1, 1)\n        test_move_grid.addWidget(self.btn_test_az_p, 1, 2)\n\n        track_calib_layout.addRow(\"Focal Length (mm):\", self.focal_spin)\n        track_calib_layout.addRow(\"Tracking Gain (Kp):\", self.kp_spin)\n        track_calib_layout.addRow(\"Deadband (px):\", self.deadband_spin)\n        track_calib_layout.addRow(\"Camera X-Axis:\", self.combo_x_axis)\n        track_calib_layout.addRow(\"\", self.cb_inv_x)\n        track_calib_layout.addRow(\"Camera Y-Axis:\", self.combo_y_axis)\n        track_calib_layout.addRow(\"\", self.cb_inv_y)\n        track_calib_layout.addRow(\"Test Move:\", test_move_widget)\n        form_adv.addRow(track_calib_group)\n\n        cam_prop = self.camera.get_camera_property()\n        bin_group = QGroupBox(\"Sensor Binning\")\n        bin_layout = QHBoxLayout(bin_group)\n        self.bin_combo = QComboBox()\n        for b in cam_prop['SupportedBins']:\n            if b != 0: self.bin_combo.addItem(f\"Bin {b}x{b}\", b)\n        self.bin_combo.setCurrentText(f\"Bin {self.current_bins}x{self.current_bins}\")\n        self.btn_apply_bin = QPushButton(\"Apply Binning\")\n        self.btn_apply_bin.clicked.connect(self.on_apply_binning)\n        bin_layout.addWidget(QLabel(\"Binning:\"))\n        bin_layout.addWidget(self.bin_combo)\n        bin_layout.addWidget(self.btn_apply_bin)\n\n        roi_group = QGroupBox(\"Sensor ROI (Resolution)\")\n        roi_layout = QGridLayout(roi_group)\n        \n        current_w = cam_prop['MaxWidth'] \/\/ self.current_bins\n        current_h = cam_prop['MaxHeight'] \/\/ self.current_bins\n        \n        self.roi_w_spin = QSpinBox()\n        self.roi_w_spin.setRange(8, current_w)\n        self.roi_w_spin.setSingleStep(8)\n        self.roi_w_spin.setValue(current_w)\n        self.roi_h_spin = QSpinBox()\n        self.roi_h_spin.setRange(2, current_h)\n        self.roi_h_spin.setSingleStep(2)\n        self.roi_h_spin.setValue(current_h)\n        self.btn_apply_roi = QPushButton(\"Apply ROI (Center)\")\n        self.btn_apply_roi.clicked.connect(self.on_apply_roi_center)\n        self.btn_reset_roi = QPushButton(\"Reset to Full Frame\")\n        self.btn_reset_roi.clicked.connect(self.on_reset_roi)\n        self.btn_reset_roi.setStyleSheet(\"background-color: #555555;\")\n        \n        roi_layout.addWidget(QLabel(\"Width:\"), 0, 0)\n        roi_layout.addWidget(self.roi_w_spin, 0, 1)\n        roi_layout.addWidget(QLabel(\"Height:\"), 1, 0)\n        roi_layout.addWidget(self.roi_h_spin, 1, 1)\n        roi_layout.addWidget(self.btn_apply_roi, 2, 0, 1, 2)\n        roi_layout.addWidget(self.btn_reset_roi, 3, 0, 1, 2)\n        \n        form_adv.addRow(roi_group)\n        form_adv.addRow(bin_group)\n\n        self.rows = []\n        for name, caps in sorted(self.camera.get_controls().items()):\n            row = ControlRow(self.camera, caps, lambda: self.statusBar().showMessage('ASI\u30d1\u30e9\u30e1\u30fc\u30bf\u66f4\u65b0', 1000))\n            self.rows.append(row)\n            # \u2605 \u30ab\u30e1\u30e9\u8a2d\u5b9a\u3082\u5168\u3066 Advanced \u3078\u79fb\u52d5\n            form_adv.addRow(name, row)\n\n        self.cam_tabs.addTab(self.tab_main, \"Main Controls\")\n        self.cam_tabs.addTab(self.tab_adv, \"Advanced\")\n        asi_layout.addWidget(self.cam_tabs)\n        \n        right_layout.addWidget(asi_group)\n        right_layout.addStretch()\n\n        right_scroll.setWidget(right_panel)\n        root.addWidget(right_scroll, stretch=2)\n        self.setStatusBar(QStatusBar())\n\n    def _update_ef_ui_state(self, connected: bool):\n        self.btn_ef_connect.setEnabled(not connected)\n        self.btn_ef_disconnect.setEnabled(connected)\n        self.btn_ef_goto.setEnabled(connected)\n        self.btn_ef_in.setEnabled(connected)\n        self.btn_ef_out.setEnabled(connected)\n\n    def on_ef_connect(self):\n        try:\n            self.ef_controller.port = self.ef_port_edit.text()\n            if self.ef_controller.connect():\n                self._update_ef_ui_state(True)\n                self.on_ef_refresh_position()\n        except: pass\n\n    def on_ef_disconnect(self):\n        self.ef_controller.disconnect()\n        self._update_ef_ui_state(False)\n\n    def on_ef_refresh_position(self):\n        if not self.ef_controller.is_connected: return\n        pos = self.ef_controller.get_position()\n        if pos is not None:\n            self.lbl_ef_position.setText(str(pos))\n            self.spin_ef_target.setValue(pos)\n\n    def on_ef_goto(self):\n        if self.ef_controller.is_connected:\n            self.ef_controller.move_absolute(self.spin_ef_target.value())\n            QTimer.singleShot(1000, self.on_ef_refresh_position)\n\n    def on_ef_relative(self, direction):\n        if self.ef_controller.is_connected:\n            target = self.spin_ef_target.value() + (self.spin_ef_rel.value() * direction)\n            self.ef_controller.move_absolute(target)\n            QTimer.singleShot(1000, self.on_ef_refresh_position)\n\n    def on_ef_set_aperture(self):\n        if self.ef_controller.is_connected:\n            self.ef_controller.set_aperture(self.spin_ef_aperture.value())\n\n    def toggle_acuter_connection(self):\n        if self.acuter.is_connected:\n            self.acuter.disconnect()\n            self.btn_acuter_connect.setText(\"\u63a5\u7d9a\")\n            self._set_acuter_controls_state(False)\n        else:\n            if self.acuter.connect(self.acuter_port_input.text()):\n                self.btn_acuter_connect.setText(\"\u5207\u65ad\")\n                self._set_acuter_controls_state(True)\n                self.acuter_poll_timer.start(500)\n\n    def _set_acuter_controls_state(self, state):\n        self.btn_az_goto.setEnabled(state)\n        self.btn_alt_goto.setEnabled(state)\n        self.btn_up.setEnabled(state)\n        self.btn_down.setEnabled(state)\n        self.btn_left.setEnabled(state)\n        self.btn_right.setEnabled(state)\n        self.btn_stop_center.setEnabled(state)\n        for btn in self.calib_buttons: btn.setEnabled(state)\n\n    def poll_acuter_position(self):\n        self.acuter.poll_position()\n        d1, d2 = self.acuter.current_deg.get(1), self.acuter.current_deg.get(2)\n        if d1 is not None: self.lbl_acuter_az.setText(f\"Az : {d1:+8.3f} \u00b0\")\n        if d2 is not None: self.lbl_acuter_alt.setText(f\"Alt: {d2:+8.3f} \u00b0\")\n        \n        if self.capture_thread:\n            self.capture_thread.camera_is_moving = self.acuter.axis_moving.get(1, False) or self.acuter.axis_moving.get(2, False)\n\n    def on_auto_track_toggled(self, checked):\n        if checked:\n            self.btn_auto_track.setText(\"Tracking (ON)\")\n            self.btn_auto_track.setStyleSheet(\"background-color: #d9534f; color: white; font-size: 18px; font-weight: bold;\")\n        else:\n            self.btn_auto_track.setText(\"Tracking (OFF)\")\n            self.btn_auto_track.setStyleSheet(\"background-color: #555; color: white; font-size: 18px; font-weight: bold;\")\n            self.acuter.emergency_stop()\n            if getattr(self, 'is_recording_event', False):\n                self.stop_event_record_and_return()\n\n    def on_motion_toggled(self, checked):\n        if self.capture_thread:\n            self.capture_thread.motion_enabled = checked\n            if not checked:\n                self.capture_thread.cv2_tracker = None\n\n    def start_event_record_and_track(self):\n        self.home_deg = {1: self.acuter.current_deg.get(1), 2: self.acuter.current_deg.get(2)}\n        self.is_recording_event = True\n        self.video_buffer = []\n        if not self.btn_auto_track.isChecked(): self.btn_auto_track.setChecked(True)\n\n    def stop_event_record_and_return(self):\n        self.is_recording_event = False\n        if self.video_buffer:\n            save_dir = \"\/Users\/mars\/acuter\/videos\"\n            os.makedirs(save_dir, exist_ok=True)\n            filename = os.path.join(save_dir, time.strftime(\"track_%Y%m%d_%H%M%S.mp4\"))\n            \n            self.statusBar().showMessage(f\"\u52d5\u753b\u3092\u4fdd\u5b58\u4e2d... {filename}\", 5000)\n            self.video_saver = VideoSaveWorker(self.video_buffer, filename, 30.0)\n            self.video_saver.finished_ok.connect(lambda f: self.statusBar().showMessage(f\"\u4fdd\u5b58\u5b8c\u4e86: {f}\", 5000))\n            self.video_saver.error.connect(lambda err: self.statusBar().showMessage(f\"\u4fdd\u5b58\u30a8\u30e9\u30fc: {err}\", 5000))\n            self.video_saver.start()\n            self.video_buffer = []\n            \n        for axis in (1, 2):\n            h_deg = self.home_deg.get(axis)\n            c_deg = self.acuter.current_deg.get(axis)\n            if h_deg is not None and c_deg is not None:\n                diff = h_deg - c_deg\n                if diff &gt; 180: diff -= 360\n                elif diff &lt; -180: diff += 360\n                if abs(diff) &gt; 0.05:\n                    self.acuter.start_move(axis, diff, exact_goto=True, speed_deg_sec=15.0)\n\n    def get_current_target_classes(self):\n        targets = []\n        if self.cb_person.isChecked(): targets.append(0)\n        if self.cb_bicycle.isChecked(): targets.append(1)\n        if self.cb_car.isChecked(): targets.append(2)\n        if self.cb_airplane.isChecked(): targets.append(4)\n        if self.cb_bird.isChecked(): targets.append(14)\n        return targets\n\n    def update_target_classes(self):\n        if self.capture_thread:\n            self.capture_thread.target_classes = self.get_current_target_classes()\n\n    def on_detection_toggled(self, checked):\n        if self.capture_thread:\n            self.capture_thread.detection_enabled = checked\n            if not checked:\n                self.target_id = None\n                self.btn_auto_track.setChecked(False)\n\n    def _on_preview_clicked(self, lx, ly):\n        scale = min(self.preview_label.width() \/ self.last_frame_size[0], self.preview_label.height() \/ self.last_frame_size[1])\n        ox = (self.preview_label.width() - self.last_frame_size[0] * scale) \/ 2\n        oy = (self.preview_label.height() - self.last_frame_size[1] * scale) \/ 2\n        fx, fy = (lx - ox) \/ scale, (ly - oy) \/ scale\n\n        clicked_id = None\n        for d in self.current_detections:\n            if d[0] &lt;= fx &lt;= d[2] and d[1] &lt;= fy &lt;= d[3]:\n                clicked_id = d[6]\n                break\n        \n        if clicked_id is not None:\n            self.target_id = clicked_id\n            if self.capture_thread: self.capture_thread.yolo_target_locked = True\n            self.lost_counter = 0  \n            if not getattr(self, 'is_recording_event', False):\n                self.start_event_record_and_track()\n            if not self.btn_auto_track.isChecked():\n                self.btn_auto_track.setChecked(True)\n            return\n\n        if getattr(self, 'last_motion_box', None) is not None:\n            mx, my, mw, mh = self.last_motion_box\n            if mx &lt;= fx &lt;= mx+mw and my &lt;= fy &lt;= my+mh:\n                self.target_id = None\n                if self.capture_thread: self.capture_thread.yolo_target_locked = False\n                if not getattr(self, 'is_recording_event', False):\n                    self.start_event_record_and_track()\n                if not self.btn_auto_track.isChecked():\n                    self.btn_auto_track.setChecked(True)\n                return\n\n        self.target_id = None\n        if self.capture_thread: self.capture_thread.yolo_target_locked = False\n        self.cb_auto_engage.setChecked(False)\n        self.btn_auto_track.setChecked(False)\n        self.acuter.emergency_stop()\n\n    def _on_roi_dragged(self, lx, ly, lw, lh): pass \n\n    def on_reset_roi(self):\n        cam_prop = self.camera.get_camera_property()\n        max_w = cam_prop['MaxWidth'] \/\/ self.current_bins\n        max_h = cam_prop['MaxHeight'] \/\/ self.current_bins\n        self.camera.set_roi(start_x=0, start_y=0, width=max_w, height=max_h, bins=self.current_bins)\n        self._start_capture()\n        \n    def on_apply_binning(self):\n        b = self.bin_combo.currentData()\n        self.camera.set_roi(bins=b)\n        self.current_bins = b\n        self._start_capture()\n        \n    def on_apply_roi_center(self):\n        w, h = (self.roi_w_spin.value() \/\/ 8) * 8, (self.roi_h_spin.value() \/\/ 2) * 2\n        cam_prop = self.camera.get_camera_property()\n        sx = ((cam_prop['MaxWidth'] \/\/ self.current_bins - w) \/\/ 2 \/\/ 4) * 4\n        sy = ((cam_prop['MaxHeight'] \/\/ self.current_bins - h) \/\/ 2 \/\/ 2) * 2\n        self.camera.set_roi(start_x=sx, start_y=sy, width=w, height=h, bins=self.current_bins)\n        self._start_capture()\n\n    def _on_frame(self, frame: np.ndarray, detections: list, motion_box: object = None):\n        self._frame_count += 1\n        self.last_motion_box = motion_box\n        h, w = frame.shape[:2]\n        self.last_frame_size = (w, h)\n        self.current_detections = detections\n        self.latest_frame = frame.copy()\n        \n        draw_frame = frame.copy()\n        cx, cy = w \/\/ 2, h \/\/ 2\n        \n        db_px = self.deadband_spin.value()\n        cv2.circle(draw_frame, (cx, cy), db_px, (255, 255, 255), 1, cv2.LINE_AA)\n        cv2.line(draw_frame, (cx-20, cy), (cx+20, cy), (255,255,255), 1)\n        cv2.line(draw_frame, (cx, cy-20), (cx, cy+20), (255,255,255), 1)\n\n        motion_cx, motion_cy = None, None\n        if motion_box is not None:\n            mx, my, mw, mh = [int(v) for v in motion_box]\n            motion_cx, motion_cy = mx + mw\/\/2, my + mh\/\/2\n            cv2.rectangle(draw_frame, (mx, my), (mx+mw, my+mh), (0, 165, 255), 2)\n            cv2.putText(draw_frame, \"MOTION\", (mx, max(my-10, 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 165, 255), 2)\n\n        if self.target_id is None and motion_box is not None and detections:\n            for d in detections:\n                dcx, dcy = (d[0]+d[2])\/\/2, (d[1]+d[3])\/\/2\n                if np.hypot(dcx - motion_cx, dcy - motion_cy) &lt; 100:\n                    self.target_id = d[6]\n                    self.last_target_name = d[5]\n                    if self.capture_thread: self.capture_thread.yolo_target_locked = True\n                    self.statusBar().showMessage(f\"Upgraded to YOLO Target: ID {self.target_id}\", 3000)\n                    break\n\n        if self.cb_auto_engage.isChecked() and self.target_id is None and detections:\n            best_det = max(detections, key=lambda d: d[4])\n            if best_det[6] is not None:\n                self.target_id = best_det[6]\n                self.last_target_name = best_det[5]\n                self.last_target_pos = ((best_det[0]+best_det[2])\/\/2, (best_det[1]+best_det[3])\/\/2)\n                self.lost_counter = 0\n                if self.capture_thread: self.capture_thread.yolo_target_locked = True\n                self.start_event_record_and_track()\n\n        if self.cb_auto_engage.isChecked() and self.target_id is None and motion_box is not None:\n            if not getattr(self, 'is_recording_event', False):\n                self.start_event_record_and_track()\n\n        is_tracking_active = False\n        tcx, tcy = None, None\n        tracking_color = (0, 255, 0)\n\n        if self.target_id is not None:\n            target_found = False\n            for d in detections:\n                if self.target_id == d[6]:\n                    target_found = True\n                    self.last_target_pos = ((d[0]+d[2])\/\/2, (d[1]+d[3])\/\/2)\n                    break\n            \n            if not target_found and self.last_target_pos:\n                best_d = None\n                min_dist = float('inf')\n                for d in detections:\n                    if d[5] == self.last_target_name:\n                        dist = np.hypot((((d[0]+d[2])\/\/2) - self.last_target_pos[0]), (((d[1]+d[3])\/\/2) - self.last_target_pos[1]))\n                        if dist &lt; min_dist:\n                            min_dist = dist\n                            best_d = d\n                if best_d is not None:\n                    self.target_id = best_d[6]\n                    target_found = True\n                    self.lost_counter = 0\n\n            if target_found:\n                tcx, tcy = self.last_target_pos\n                is_tracking_active = True\n                tracking_color = (0, 0, 255)\n                self.lost_counter = 0\n            else:\n                self.lost_counter += 1\n                if self.last_target_pos:\n                    gx, gy = self.last_target_pos\n                    cv2.circle(draw_frame, (gx, gy), 15, (0,165,255), 2)\n                    cv2.putText(draw_frame, \"SEARCHING...\", (gx+20, gy), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,165,255), 2)\n                \n                if self.lost_counter == 1:\n                    self.acuter.emergency_stop()\n                    self.debug_info_status = \"Status: Target Lost (Braking)\"\n                \n                if self.lost_counter &gt; 150:\n                    self.debug_info_status = \"Status: Target completely LOST! (Aborted)\"\n                    self.acuter.emergency_stop()\n                    self.btn_auto_track.setChecked(False)\n                    self.target_id = None\n                    if self.capture_thread: self.capture_thread.yolo_target_locked = False\n                    self.last_target_pos = None\n                    self.lost_counter = 0\n\n        elif motion_box is not None:\n            tcx, tcy = motion_cx, motion_cy\n            is_tracking_active = True\n            tracking_color = (0, 165, 255)\n\n        for d in detections:\n            tid = d[6]\n            is_target = (self.target_id is not None and tid == self.target_id)\n            color = (0,0,255) if is_target else (0,255,0)\n            thickness = 4 if is_target else 2\n            \n            cv2.rectangle(draw_frame, (d[0], d[1]), (d[2], d[3]), color, thickness)\n            \n            id_str = f\"ID:{tid} \" if tid is not None else \"\"\n            label_text = f\"{id_str}{d[5]} {d[4]*100:.1f}%\"\n            if is_target:\n                label_text = \"[LOCKED] \" + label_text\n            \n            cv2.putText(draw_frame, label_text, (d[0], max(d[1] - 10, 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)\n\n        if is_tracking_active and tcx is not None:\n            cv2.line(draw_frame, (int(tcx) - 15, int(tcy)), (int(tcx) + 15, int(tcy)), tracking_color, 2)\n            cv2.line(draw_frame, (int(tcx), int(tcy) - 15), (int(tcx), int(tcy) + 15), tracking_color, 2)\n            cv2.line(draw_frame, (int(tcx), int(tcy)), (cx, cy), (0, 255, 255), 1)\n            \n            if self.btn_auto_track.isChecked() and self.calib_state == 0:\n                self._update_tracking(tcx, tcy, w, h)\n\n        if getattr(self, 'is_recording_event', False):\n            cv2.circle(draw_frame, (40, 40), 12, (0, 0, 255), -1)\n            cv2.putText(draw_frame, \"REC\", (60, 48), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3)\n            self.video_buffer.append(draw_frame.copy())\n\n        cv2.putText(draw_frame, getattr(self, 'debug_info_dxdy', ''), (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)\n        cv2.putText(draw_frame, getattr(self, 'debug_info_azalt', ''), (10, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)\n        cv2.putText(draw_frame, getattr(self, 'debug_info_status', ''), (10, 150), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)\n\n        qimg = QImage(draw_frame.data, w, h, 3*w, QImage.Format.Format_BGR888)\n        self.preview_label.setPixmap(QPixmap.fromImage(qimg).scaled(self.preview_label.size(), Qt.AspectRatioMode.KeepAspectRatio))\n\n    def _update_tracking(self, tcx, tcy, w, h):\n        if not self.acuter.is_connected or time.time() &lt; self.next_track_time: return\n        self.next_track_time = time.time() + 0.1 \n        \n        dx, dy = tcx - w\/2.0, tcy - h\/2.0\n        db = self.deadband_spin.value()\n        \n        if abs(dx) &lt; db and abs(dy) &lt; db:\n            if self.acuter.axis_moving.get(1) or self.acuter.axis_moving.get(2):\n                self.acuter.emergency_stop()\n            self.debug_info_status = f\"Status: Target Centered (&lt;{db}px)\"\n            return\n            \n        deg_per_px = (self.pixel_size * self.current_bins \/ self.focal_spin.value()) * (180.0 \/ np.pi)\n        vx = dx * deg_per_px * self.kp_spin.value() * (-1.0 if self.cb_inv_x.isChecked() else 1.0)\n        vy = dy * deg_per_px * self.kp_spin.value() * (-1.0 if self.cb_inv_y.isChecked() else 1.0)\n\n        az_deg = vx if self.combo_x_axis.currentIndex() == 0 else vy\n        alt_deg = vx if self.combo_x_axis.currentIndex() == 1 else vy\n\n        spd = float(self.acuter_speed_combo.currentText())\n        db_deg = db * deg_per_px * self.kp_spin.value()\n\n        self.debug_info_dxdy = f\"Target diff: dx={dx:.1f}px, dy={dy:.1f}px\"\n\n        p_gain = 0.5 \n        speed_az = max(0.1, min(spd, abs(az_deg) * p_gain))\n        speed_alt = max(0.1, min(spd, abs(alt_deg) * p_gain))\n\n        sent = False\n        \n        if abs(az_deg) &gt; db_deg:\n            self.acuter.start_move(1, az_deg, exact_goto=False, speed_deg_sec=speed_az)\n            sent = True\n        elif self.acuter.axis_moving.get(1):\n            self.acuter.stop_axis(1)\n\n        if abs(alt_deg) &gt; db_deg:\n            self.acuter.start_move(2, alt_deg, exact_goto=False, speed_deg_sec=speed_alt)\n            sent = True\n        elif self.acuter.axis_moving.get(2):\n            self.acuter.stop_axis(2)\n\n        if sent:\n            self.debug_info_azalt = f\"Speed: Az {speed_az:.1f} d\/s, Alt {speed_alt:.1f} d\/s\"\n            self.debug_info_status = \"Status: Continuous P-Control Tracking\"\n\n    def start_auto_calib(self):\n        if not self.acuter.is_connected or self.latest_frame is None: return\n        self.btn_auto_track.setChecked(False)\n        self.calib_state = 1\n        self.calib_timer.start(1000)\n\n    def stop_calib(self, msg):\n        self.calib_state = 0\n        self.calib_timer.stop()\n        self.statusBar().showMessage(msg, 5000)\n\n    def calib_step(self):\n        if self.latest_frame is None: return\n        gray = cv2.cvtColor(self.latest_frame, cv2.COLOR_BGR2GRAY)\n        if self.calib_state == 1:\n            self.calib_pts_prev = cv2.goodFeaturesToTrack(gray, maxCorners=100, qualityLevel=0.01, minDistance=30)\n            if self.calib_pts_prev is None: return self.stop_calib(\"Failed\")\n            self.calib_gray_prev = gray\n            self.acuter.start_move(1, 1.0, exact_goto=True, speed_deg_sec=2.0)\n            self.calib_wait = 3\n            self.calib_state = 2\n        elif self.calib_state == 2:\n            self.calib_wait -= 1\n            if self.calib_wait &lt;= 0:\n                pts_next, status, _ = cv2.calcOpticalFlowPyrLK(self.calib_gray_prev, gray, self.calib_pts_prev, None)\n                diff = pts_next[status == 1] - self.calib_pts_prev[status == 1]\n                self.calib_dx1, self.calib_dy1 = np.median(diff[:, 0]), np.median(diff[:, 1])\n                self.acuter.start_move(1, -1.0, exact_goto=True, speed_deg_sec=2.0)\n                self.calib_wait = 3; self.calib_state = 3\n        elif self.calib_state == 3:\n            self.calib_wait -= 1\n            if self.calib_wait &lt;= 0:\n                self.calib_pts_prev = cv2.goodFeaturesToTrack(gray, maxCorners=100, qualityLevel=0.01, minDistance=30)\n                self.calib_gray_prev = gray\n                self.acuter.start_move(2, 1.0, exact_goto=True, speed_deg_sec=2.0)\n                self.calib_wait = 3; self.calib_state = 4\n        elif self.calib_state == 4:\n            self.calib_wait -= 1\n            if self.calib_wait &lt;= 0:\n                pts_next, status, _ = cv2.calcOpticalFlowPyrLK(self.calib_gray_prev, gray, self.calib_pts_prev, None)\n                diff = pts_next[status == 1] - self.calib_pts_prev[status == 1]\n                self.calib_dx2, self.calib_dy2 = np.median(diff[:, 0]), np.median(diff[:, 1])\n                self.acuter.start_move(2, -1.0, exact_goto=True, speed_deg_sec=2.0)\n                self.calib_wait = 3; self.calib_state = 5\n        elif self.calib_state == 5:\n            self.calib_wait -= 1\n            if self.calib_wait &lt;= 0:\n                if abs(self.calib_dx1) &gt; abs(self.calib_dy1):\n                    self.combo_x_axis.setCurrentIndex(0); self.combo_y_axis.setCurrentIndex(0)\n                    self.cb_inv_x.setChecked(bool(self.calib_dx1 &gt; 0))\n                    self.cb_inv_y.setChecked(bool(self.calib_dy2 &gt; 0))\n                else:\n                    self.combo_x_axis.setCurrentIndex(1); self.combo_y_axis.setCurrentIndex(1)\n                    self.cb_inv_x.setChecked(bool(self.calib_dx2 &gt; 0))\n                    self.cb_inv_y.setChecked(bool(self.calib_dy1 &gt; 0))\n                self.stop_calib(\"Success\")\n\n    def closeEvent(self, event):\n        self.acuter.disconnect()\n        self.ef_controller.disconnect()\n        if self.capture_thread: self.capture_thread.stop()\n        self.camera.close()\n        super().closeEvent(event)\n\nif __name__ == '__main__':\n    app = QApplication(sys.argv)\n    dark_stylesheet = \"\"\"\n        QMainWindow { background-color: #2b2b2b; } QLabel { color: #e0e0e0; font-size: 13px; }\n        QGroupBox { color: #e0e0e0; border: 1px solid #555; border-radius: 6px; margin-top: 16px; padding-top: 15px; font-weight: bold; }\n        QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; left: 10px; padding: 0 5px; }\n        QTabWidget::pane { border: 1px solid #555; } QTabBar::tab { background: #3c3c3c; color: white; padding: 8px 12px; }\n        QTabBar::tab:selected { background: #5c9eff; color: black; font-weight: bold; }\n        QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox { background-color: #3c3c3c; color: white; border: 1px solid #555; padding: 4px; border-radius: 4px; }\n        QCheckBox { color: #e0e0e0; }\n        QPushButton { background-color: #3c3c3c; color: #ffffff; border: 1px solid #555; border-radius: 4px; padding: 6px; font-weight: bold; }\n        QPushButton:pressed { background-color: #5c9eff; color: #000; }\n    \"\"\"\n    app.setStyleSheet(dark_stylesheet) \n    window = MainWindow(\"\/Users\/mars\/acuter\/ASI_Camera_SDK\/ASI_linux_mac_SDK_V1.41\/lib\/mac_arm64\/libASICamera2.dylib\")\n    window.show()\n    sys.exit(app.exec())<\/code><\/pre><\/div>\n\n<\/div>\n\t\t<\/div>\n<\/div>\n\n<div class=\"wp-block-ub-content-toggle wp-block-ub-content-toggle-block\" id=\"ub-content-toggle-block-62f11965-16bc-4de6-9caa-6e7164e23a88\" data-mobilecollapse=\"false\" data-desktopcollapse=\"false\" data-preventcollapse=\"false\" data-showonlyone=\"false\">\n<div class=\"wp-block-ub-content-toggle-accordion\" style=\"border-color: #f1f1f1; border-top-left-radius: 8px; border-top-right-radius: 8px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; \" id=\"ub-content-toggle-panel-block-\">\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-title-wrap\" style=\"background-color: #f1f1f1;\" aria-controls=\"ub-content-toggle-panel-0-62f11965-16bc-4de6-9caa-6e7164e23a88\" tabindex=\"0\">\n\t\t\t<p class=\"wp-block-ub-content-toggle-accordion-title ub-content-toggle-title-62f11965-16bc-4de6-9caa-6e7164e23a88\" style=\"color: #000000; \"><strong>\/Users\/mars\/acuter\/device_controllers.py<\/strong><\/p>\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-toggle-wrap right\" style=\"color: #000000;\"><span class=\"wp-block-ub-content-toggle-accordion-state-indicator wp-block-ub-chevron-down open\"><\/span><\/div>\n\t\t<\/div>\n\t\t\t<div role=\"region\" aria-expanded=\"true\" class=\"wp-block-ub-content-toggle-accordion-content-wrap\" id=\"ub-content-toggle-panel-0-62f11965-16bc-4de6-9caa-6e7164e23a88\">\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-plain\"><code>import time\nimport re\nimport serial\nfrom typing import Optional\nfrom PyQt6.QtCore import QThread, pyqtSignal\n\ndef hex_le_to_int(hex_str: str) -&gt; int:\n    if len(hex_str) % 2 != 0:\n        hex_str = \"0\" + hex_str\n    reversed_hex = \"\".join([hex_str[i:i+2] for i in range(0, len(hex_str), 2)][::-1])\n    return int(reversed_hex, 16)\n\ndef int_to_hex_le(val: int, length_chars: int) -&gt; str:\n    hex_str = f\"{val:0{length_chars}X}\"\n    reversed_hex = \"\".join([hex_str[i:i+2] for i in range(0, len(hex_str), 2)][::-1])\n    return reversed_hex\n\nclass AcuterController:\n    def __init__(self):\n        self.ser = None\n        self.cpr = {1: 1017435, 2: 1017435}\n        self.timer_freq = {1: 16000000, 2: 16000000}\n        self.current_deg = {1: None, 2: None}\n        self.axis_moving = {1: False, 2: False}\n        self.current_period = {1: None, 2: None}\n        self.current_dir = {1: None, 2: None}\n\n    @property\n    def is_connected(self):\n        return self.ser is not None and self.ser.is_open\n\n    def connect(self, port: str, baudrate: int = 115200) -&gt; bool:\n        try:\n            self.ser = serial.Serial(port, baudrate, timeout=0.05)\n            for axis in (1, 2):\n                cpr_hex = self._send_cmd(f\":a{axis}\\r\")\n                timer_hex = self._send_cmd(f\":b{axis}\\r\")\n                if cpr_hex and \"Error\" not in cpr_hex:\n                    self.cpr[axis] = hex_le_to_int(cpr_hex)\n                if timer_hex and \"Error\" not in timer_hex:\n                    self.timer_freq[axis] = hex_le_to_int(timer_hex)\n            return True\n        except Exception as e:\n            self.disconnect()\n            raise e\n\n    def disconnect(self):\n        if self.is_connected:\n            self.emergency_stop()\n            self.ser.close()\n        self.ser = None\n\n    def _send_cmd(self, cmd: str) -&gt; str:\n        if not self.is_connected: return \"\"\n        try:\n            self.ser.reset_input_buffer()\n            self.ser.write(cmd.encode('ascii'))\n            resp_bytes = self.ser.read_until(b'\\r')\n            resp = resp_bytes.decode('ascii', errors='ignore').strip()\n            return resp[1:] if resp.startswith('=') else resp\n        except Exception:\n            return \"\"\n\n    def poll_position(self):\n        if not self.is_connected: return\n        for axis in (1, 2):\n            pos_hex = self._send_cmd(f\":j{axis}\\r\")\n            if pos_hex and \"Error\" not in pos_hex and len(pos_hex) &gt;= 5:\n                try:\n                    current_pos = hex_le_to_int(pos_hex)\n                    center = 0x800000 if len(pos_hex) &lt;= 6 else 0x80000000\n                    deg = ((current_pos - center) \/ float(self.cpr[axis])) * 360.0\n                    self.current_deg[axis] = (deg + 180) % 360 - 180\n                except ValueError:\n                    pass\n\n    def start_move(self, axis: int, angle_degrees: float, exact_goto: bool = False, speed_deg_sec: float = 10.0):\n        if not self.is_connected or speed_deg_sec &lt;= 0 or angle_degrees == 0: return\n        try:\n            dir_char = \"0\" if angle_degrees &gt;= 0 else \"1\"\n            steps_per_sec = (speed_deg_sec \/ 360.0) * self.cpr[axis]\n            period = max(1, int(self.timer_freq[axis] \/ steps_per_sec))\n            period_hex_le = int_to_hex_le(period, 6)\n            \n            if exact_goto:\n                pos_hex = self._send_cmd(f\":j{axis}\\r\")\n                if not pos_hex or \"Error\" in pos_hex: return\n                cur_pos = hex_le_to_int(pos_hex)\n                pos_len = len(pos_hex)\n                offset_steps = int(self.cpr[axis] * (angle_degrees \/ 360.0))\n                max_val = 1 &lt;&lt; (pos_len * 4)\n                target_pos = (cur_pos + offset_steps) % max_val\n                target_hex_le = int_to_hex_le(target_pos, pos_len)\n                \n                mode = \"0\" + dir_char \n                self._send_cmd(f\":S{axis}{target_hex_le}\\r\")\n                self._send_cmd(f\":I{axis}{period_hex_le}\\r\")\n                self._send_cmd(f\":G{axis}{mode}\\r\")\n                self._send_cmd(f\":J{axis}\\r\")\n            else:\n                if self.axis_moving.get(axis, False):\n                    prev_dir = self.current_dir.get(axis)\n                    prev_period = self.current_period.get(axis)\n                    \n                    if prev_dir == dir_char and prev_period is not None:\n                        if abs(period - prev_period) \/ float(prev_period) &lt; 0.15:\n                            return\n                    elif prev_dir != dir_char:\n                        self.ser.reset_output_buffer()\n                        self._send_cmd(f\":L{axis}\\r\")\n                        time.sleep(0.05)\n                \n                self.current_dir[axis] = dir_char\n                self.current_period[axis] = period\n                self.axis_moving[axis] = True\n\n                mode = \"3\" + dir_char \n                self._send_cmd(f\":I{axis}{period_hex_le}\\r\")\n                self._send_cmd(f\":G{axis}{mode}\\r\")\n                self._send_cmd(f\":J{axis}\\r\")\n        except Exception:\n            pass\n\n    def stop_axis(self, axis: int):\n        if not self.is_connected: return\n        if self.axis_moving.get(axis, False):\n            self.ser.reset_output_buffer() \n            self._send_cmd(f\":L{axis}\\r\")\n            self.axis_moving[axis] = False\n            self.current_period[axis] = None\n            self.current_dir[axis] = None\n\n    def emergency_stop(self):\n        if not self.is_connected: return\n        self.ser.reset_output_buffer()\n        self._send_cmd(\":L1\\r\")\n        self._send_cmd(\":L2\\r\")\n        self.axis_moving = {1: False, 2: False}\n        self.current_period = {1: None, 2: None}\n        self.current_dir = {1: None, 2: None}\n\nclass AstromechanicsEFController:\n    def __init__(self, port: str = \"\", baudrate: int = 38400, timeout: float = 1.0):\n        self.port = port\n        self.baudrate = baudrate\n        self.timeout = timeout\n        self.ser: Optional[serial.Serial] = None\n\n    def connect(self) -&gt; bool:\n        try:\n            self.ser = serial.Serial(self.port, self.baudrate, timeout=self.timeout)\n            time.sleep(2)\n            self.ser.reset_input_buffer()\n            return self.get_position() is not None\n        except Exception as e:\n            self.ser = None\n            raise e\n\n    def disconnect(self):\n        if self.ser and self.ser.is_open:\n            self.ser.close()\n        self.ser = None\n\n    @property\n    def is_connected(self) -&gt; bool:\n        return self.ser is not None and self.ser.is_open\n\n    def _send(self, cmd: str, expect_reply: bool = False) -&gt; Optional[str]:\n        if not self.is_connected: return None\n        if not cmd.endswith(\"#\"): cmd += \"#\"\n        self.ser.reset_input_buffer()\n        self.ser.write(cmd.encode(\"ascii\"))\n        self.ser.flush()\n\n        if not expect_reply:\n            time.sleep(0.1)\n            return None\n\n        response = b\"\"\n        start = time.time()\n        while time.time() - start &lt; self.timeout:\n            if self.ser.in_waiting:\n                response += self.ser.read(self.ser.in_waiting)\n                if b\"#\" in response: break\n            time.sleep(0.1)\n\n        text = response.decode(\"ascii\", errors=\"ignore\").strip()\n        m = re.search(r\"(\\d+)#?\", text)\n        return m.group(1) if m else None\n\n    def get_position(self) -&gt; Optional[int]:\n        reply = self._send(\"P#\", expect_reply=True)\n        return int(reply) if reply else None\n\n    def move_absolute(self, position: int):\n        self._send(f\"M{position}#\", expect_reply=False)\n\n    def set_aperture(self, index: int):\n        self._send(f\"A{index:02d}#\", expect_reply=False)\n\nclass PositionWorker(QThread):\n    position_ready = pyqtSignal(int)\n    error = pyqtSignal(str)\n    def __init__(self, controller):\n        super().__init__()\n        self.controller = controller\n    def run(self):\n        try:\n            pos = self.controller.get_position()\n            if pos is not None: self.position_ready.emit(pos)\n            else: self.error.emit(\"\u4f4d\u7f6e\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\")\n        except Exception as e:\n            self.error.emit(str(e))\n\nclass MoveWorker(QThread):\n    finished_ok = pyqtSignal(int)\n    error = pyqtSignal(str)\n    progress = pyqtSignal(int)\n    def __init__(self, controller, target, tolerance=5):\n        super().__init__()\n        self.controller = controller\n        self.target = target\n        self.tolerance = tolerance\n    def run(self):\n        try:\n            self.controller.move_absolute(self.target)\n            start = time.time()\n            while time.time() - start &lt; 30.0:\n                pos = self.controller.get_position()\n                if pos is None:\n                    time.sleep(0.2)\n                    continue\n                self.progress.emit(pos)\n                if abs(pos - self.target) &lt;= self.tolerance:\n                    self.finished_ok.emit(pos)\n                    return\n                time.sleep(0.25)\n            self.error.emit(\"\u79fb\u52d5\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\")\n        except Exception as e:\n            self.error.emit(str(e))<\/code><\/pre><\/div>\n\n<\/div>\n\t\t<\/div>\n<\/div>\n\n<div class=\"wp-block-ub-content-toggle wp-block-ub-content-toggle-block\" id=\"ub-content-toggle-block-38c3ae55-0f5f-482f-9a1c-574684272e97\" data-mobilecollapse=\"false\" data-desktopcollapse=\"false\" data-preventcollapse=\"false\" data-showonlyone=\"false\">\n<div class=\"wp-block-ub-content-toggle-accordion\" style=\"border-color: #f1f1f1; border-top-left-radius: 8px; border-top-right-radius: 8px; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; \" id=\"ub-content-toggle-panel-block-\">\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-title-wrap\" style=\"background-color: #f1f1f1;\" aria-controls=\"ub-content-toggle-panel-0-38c3ae55-0f5f-482f-9a1c-574684272e97\" tabindex=\"0\">\n\t\t\t<p class=\"wp-block-ub-content-toggle-accordion-title ub-content-toggle-title-38c3ae55-0f5f-482f-9a1c-574684272e97\" style=\"color: #000000; \"><strong>\/Users\/mars\/acuter\/camera_yolo.py<\/strong><\/p>\n\t\t\t<div class=\"wp-block-ub-content-toggle-accordion-toggle-wrap right\" style=\"color: #000000;\"><span class=\"wp-block-ub-content-toggle-accordion-state-indicator wp-block-ub-chevron-down open\"><\/span><\/div>\n\t\t<\/div>\n\t\t\t<div role=\"region\" aria-expanded=\"true\" class=\"wp-block-ub-content-toggle-accordion-content-wrap\" id=\"ub-content-toggle-panel-0-38c3ae55-0f5f-482f-9a1c-574684272e97\">\n\n<div class=\"hcb_wrap\"><pre class=\"prism line-numbers lang-plain\"><code>import os\nimport time\nimport urllib.request\nimport cv2\nimport numpy as np\nimport zwoasi as asi\nfrom ultralytics import YOLO\n\nfrom PyQt6.QtCore import Qt, QThread, pyqtSignal\nfrom PyQt6.QtGui import QPainter, QPen, QColor\nfrom PyQt6.QtWidgets import QLabel, QWidget, QHBoxLayout, QSlider, QSpinBox, QCheckBox\n\nYOLO_MODEL_URL = \"https:\/\/github.com\/ultralytics\/assets\/releases\/download\/v8.4.0\/yolov8n.pt\"\nYOLO_MODEL_FILE = \"\/Users\/mars\/acuter\/yolov8n.pt\"\n\ndef download_model_files():\n    if not os.path.exists(YOLO_MODEL_FILE):\n        print(f\"Downloading {YOLO_MODEL_FILE} from GitHub. Please wait...\")\n        try:\n            os.makedirs(os.path.dirname(YOLO_MODEL_FILE), exist_ok=True)\n            urllib.request.urlretrieve(YOLO_MODEL_URL, YOLO_MODEL_FILE)\n            print(\"Download completed successfully.\")\n        except Exception as e:\n            print(f\"Error downloading the model: {e}\")\n\ndef create_tracker():\n    try:\n        return cv2.TrackerKCF_create()\n    except AttributeError:\n        try:\n            return cv2.TrackerMIL_create()\n        except AttributeError:\n            try:\n                return cv2.legacy.TrackerKCF_create()\n            except AttributeError:\n                return None\n\nclass VideoLabel(QLabel):\n    clicked = pyqtSignal(int, int)\n    roi_selected = pyqtSignal(int, int, int, int)\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.start_point = None\n        self.end_point = None\n        self.is_drawing = False\n\n    def mousePressEvent(self, event):\n        if event.button() == Qt.MouseButton.LeftButton:\n            self.start_point = event.position().toPoint()\n            self.end_point = self.start_point\n            self.is_drawing = True\n        super().mousePressEvent(event)\n\n    def mouseMoveEvent(self, event):\n        if self.is_drawing:\n            self.end_point = event.position().toPoint()\n            self.update()\n        super().mouseMoveEvent(event)\n\n    def mouseReleaseEvent(self, event):\n        if event.button() == Qt.MouseButton.LeftButton and self.is_drawing:\n            self.end_point = event.position().toPoint()\n            self.is_drawing = False\n            self.update()\n            dist = (self.start_point.x() - self.end_point.x())**2 + (self.start_point.y() - self.end_point.y())**2\n            if dist &lt; 25: \n                self.clicked.emit(self.start_point.x(), self.start_point.y())\n            else:\n                x1 = min(self.start_point.x(), self.end_point.x())\n                y1 = min(self.start_point.y(), self.end_point.y())\n                w = abs(self.start_point.x() - self.end_point.x())\n                h = abs(self.start_point.y() - self.end_point.y())\n                self.roi_selected.emit(x1, y1, w, h)\n            self.start_point = None\n            self.end_point = None\n        super().mouseReleaseEvent(event)\n\n    def paintEvent(self, event):\n        super().paintEvent(event)\n        if self.is_drawing and self.start_point and self.end_point:\n            painter = QPainter(self)\n            pen = QPen(QColor(0, 255, 255))\n            pen.setWidth(2)\n            pen.setStyle(Qt.PenStyle.DashLine)\n            painter.setPen(pen)\n            x = min(self.start_point.x(), self.end_point.x())\n            y = min(self.start_point.y(), self.end_point.y())\n            w = abs(self.start_point.x() - self.end_point.x())\n            h = abs(self.start_point.y() - self.end_point.y())\n            painter.drawRect(x, y, w, h)\n            painter.end()\n\nclass CaptureThread(QThread):\n    frame_ready = pyqtSignal(np.ndarray, list, object)\n    error = pyqtSignal(str)\n\n    def __init__(self, camera, model_file, parent=None):\n        super().__init__(parent)\n        self.camera = camera\n        self._running = False\n        self.model_file = model_file\n        self.yolo_model = None\n        self.target_classes = []\n        self.detection_enabled = False\n        \n        self.motion_enabled = False\n        self.camera_is_moving = False\n        self.cv2_tracker = None\n        self.yolo_target_locked = False\n        self.prev_frame = None\n\n    def run(self):\n        self._running = True\n        try:\n            self.yolo_model = YOLO(self.model_file)\n        except Exception as e:\n            print(f\"Failed to load YOLO model: {e}\")\n            self.yolo_model = None\n\n        self.camera.start_video_capture()\n        try:\n            while self._running:\n                try:\n                    frame = self.camera.capture_video_frame(timeout=2000)\n                except asi.ZWO_Error:\n                    continue\n                except Exception as exc: \n                    self.error.emit(str(exc))\n                    continue\n                \n                detections = []\n                if self.detection_enabled and self.yolo_model is not None and self.target_classes:\n                    try:\n                        results = self.yolo_model.track(\n                            frame, imgsz=480, classes=self.target_classes, \n                            persist=True, verbose=False, tracker=\"bytetrack.yaml\"\n                        )\n                        for r in results:\n                            for box in r.boxes:\n                                x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int)\n                                conf = float(box.conf[0])\n                                cls_id = int(box.cls[0])\n                                name = self.yolo_model.names[cls_id]\n                                track_id = int(box.id[0]) if box.id is not None else None\n                                if conf &gt; 0.25:\n                                    detections.append((x1, y1, x2, y2, conf, name, track_id))\n                    except Exception:\n                        pass\n\n                motion_box = None\n                if self.yolo_target_locked:\n                    self.cv2_tracker = None\n                    self.prev_frame = None\n                elif self.motion_enabled:\n                    if self.cv2_tracker is not None:\n                        success, bbox = self.cv2_tracker.update(frame)\n                        if success:\n                            motion_box = bbox\n                        else:\n                            self.cv2_tracker = None\n                    \n                    if self.cv2_tracker is None:\n                        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)\n                        if self.prev_frame is None or self.camera_is_moving or self.prev_frame.shape != gray.shape:\n                            self.prev_frame = gray.copy()\n                        else:\n                            diff = cv2.absdiff(self.prev_frame, gray)\n                            self.prev_frame = gray.copy()\n                            _, thresh = cv2.threshold(diff, 20, 255, cv2.THRESH_BINARY)\n                            thresh = cv2.dilate(thresh, None, iterations=2)\n                            contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)\n                            \n                            best_area = 0\n                            best_bbox = None\n                            for c in contours:\n                                area = cv2.contourArea(c)\n                                if 15 &lt; area &lt; 10000:\n                                    if area &gt; best_area:\n                                        best_area = area\n                                        best_bbox = cv2.boundingRect(c)\n                            \n                            if best_bbox is not None:\n                                self.cv2_tracker = create_tracker()\n                                if self.cv2_tracker is not None:\n                                    try:\n                                        self.cv2_tracker.init(frame, best_bbox)\n                                        motion_box = best_bbox\n                                    except Exception:\n                                        self.cv2_tracker = None\n\n                self.frame_ready.emit(frame.copy(), detections, motion_box)\n        finally:\n            try:\n                self.camera.stop_video_capture()\n            except:\n                pass\n\n    def stop(self):\n        self._running = False\n        self.wait(2000)\n\nclass VideoSaveWorker(QThread):\n    finished_ok = pyqtSignal(str)\n    error = pyqtSignal(str)\n\n    def __init__(self, frames, filename, fps):\n        super().__init__()\n        self.frames = frames\n        self.filename = filename\n        self.fps = fps\n\n    def run(self):\n        try:\n            if not self.frames:\n                self.error.emit(\"\u4fdd\u5b58\u3059\u308b\u30d5\u30ec\u30fc\u30e0\u304c\u3042\u308a\u307e\u305b\u3093\")\n                return\n            h, w = self.frames[0].shape[:2]\n            fourcc = cv2.VideoWriter_fourcc(*'mp4v')\n            out = cv2.VideoWriter(self.filename, fourcc, self.fps, (w, h))\n            for f in self.frames: out.write(f)\n            out.release()\n            self.finished_ok.emit(self.filename)\n        except Exception as e:\n            self.error.emit(str(e))\n\nclass ControlRow(QWidget):\n    def __init__(self, camera, caps, on_change, parent=None):\n        super().__init__(parent)\n        self.camera = camera\n        self.caps = caps\n        self.on_change = on_change\n        self._updating = False\n\n        control_type = caps['ControlType']\n        min_v, max_v, default_v = caps['MinValue'], caps['MaxValue'], caps['DefaultValue']\n        if control_type == 1:\n            max_v = 40000\n            if default_v &gt; 40000: default_v = 40000\n\n        current_v, is_auto = camera.get_control_value(control_type)\n        if control_type == 1 and current_v &gt; 40000: current_v = 40000\n\n        layout = QHBoxLayout(self)\n        layout.setContentsMargins(0, 0, 0, 0)\n\n        self.slider = QSlider(Qt.Orientation.Horizontal)\n        self.slider.setMinimum(min_v)\n        self.slider.setMaximum(max_v)\n        self.slider.setValue(current_v)\n\n        self.spin = QSpinBox()\n        self.spin.setMinimum(min_v)\n        self.spin.setMaximum(max_v)\n        self.spin.setMaximumWidth(120) \n        self.spin.setValue(current_v)\n\n        self.auto_box = QCheckBox('Auto')\n        self.auto_box.setChecked(is_auto)\n        self.auto_box.setEnabled(bool(caps['IsAutoSupported']))\n\n        layout.addWidget(self.slider, stretch=1)\n        layout.addWidget(self.spin)\n        layout.addWidget(self.auto_box)\n\n        self.slider.valueChanged.connect(self._on_slider)\n        self.spin.valueChanged.connect(self._on_spin)\n        self.auto_box.toggled.connect(self._on_auto)\n\n        if not caps['IsWritable']: self.setEnabled(False)\n\n    def _sync(self, value):\n        self._updating = True\n        self.slider.setValue(value)\n        self.spin.setValue(value)\n        self._updating = False\n\n    def _push(self, value, auto):\n        self.camera.set_control_value(self.caps['ControlType'], value, auto)\n        self.on_change()\n\n    def _on_slider(self, value):\n        if self._updating: return\n        self._sync(value)\n        self._push(value, self.auto_box.isChecked())\n\n    def _on_spin(self, value):\n        if self._updating: return\n        self._sync(value)\n        self._push(value, self.auto_box.isChecked())\n\n    def _on_auto(self, checked):\n        self._push(self.spin.value(), checked)\n\nimport sys\nimport os\nimport time\nimport cv2\nimport numpy as np\nimport zwoasi as asi\n\nfrom PyQt6.QtCore import Qt, QTimer\nfrom PyQt6.QtGui import QImage, QPixmap\nfrom PyQt6.QtWidgets import (\n    QApplication, QCheckBox, QFormLayout, QGroupBox, QHBoxLayout, QLabel,\n    QMainWindow, QScrollArea, QSpinBox, QDoubleSpinBox, QStatusBar, \n    QVBoxLayout, QWidget, QLineEdit, QPushButton, QMessageBox, QComboBox, \n    QGridLayout, QTabWidget\n)\n\nfrom device_controllers import AcuterController, AstromechanicsEFController, PositionWorker, MoveWorker\nfrom camera_yolo import download_model_files, YOLO_MODEL_FILE, VideoLabel, CaptureThread, ControlRow, VideoSaveWorker\n\nclass MainWindow(QMainWindow):\n    def __init__(self, sdk_path):\n        super().__init__()\n        self.setWindowTitle('ZWO ASI Camera + EF Lens + Acuter + YOLOv8 Complete (v2.39.0)')\n        self.resize(1300, 900)\n\n        asi.init(sdk_path)\n        if asi.get_num_cameras() == 0:\n            raise RuntimeError('ZWO\u30ab\u30e1\u30e9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093')\n\n        self.camera = asi.Camera(0)\n        self.camera.set_image_type(asi.ASI_IMG_RGB24)\n        \n        cam_prop = self.camera.get_camera_property()\n        supported_bins = cam_prop.get('SupportedBins', [1])\n        self.current_bins = 2 if 2 in supported_bins else 1\n        current_w = cam_prop['MaxWidth'] \/\/ self.current_bins\n        current_h = cam_prop['MaxHeight'] \/\/ self.current_bins\n        \n        self.camera.set_roi(start_x=0, start_y=0, width=current_w, height=current_h, bins=self.current_bins)\n        self.current_start_x = 0\n        self.current_start_y = 0\n\n        self.target_id = None\n        self.last_target_pos = None\n        self.last_target_name = None\n        self.current_detections = []\n        self.last_frame_size = (current_w, current_h)\n        self.latest_frame = None\n        self.last_motion_box = None\n\n        self.pixel_size = 0.0038\n        self.next_track_time = 0.0\n        self.lost_counter = 0\n        \n        self.is_recording_event = False\n        self.video_buffer = []\n        self.home_deg = {1: None, 2: None}\n        \n        self.debug_info_dxdy = \"Target diff: N\/A\"\n        self.debug_info_azalt = \"Cmd: N\/A\"\n        self.debug_info_status = \"Status: Idle\"\n        \n        self.calib_state = 0\n        self.calib_timer = QTimer(self)\n        self.calib_timer.timeout.connect(self.calib_step)\n        self.calib_pts_prev = None\n        self.calib_gray_prev = None\n\n        self.acuter = AcuterController()\n        self.acuter_poll_timer = QTimer(self)\n        self.acuter_poll_timer.timeout.connect(self.poll_acuter_position)\n\n        self.ef_controller = AstromechanicsEFController()\n        self.capture_thread = None\n\n        self._build_ui()\n        self._update_ef_ui_state(False)\n        self._set_acuter_controls_state(False)\n\n        self._frame_count = 0\n        self._start_capture()\n        QTimer.singleShot(500, self.auto_connect_devices)\n\n    def auto_connect_devices(self):\n        port_ef = self.ef_port_edit.text().strip()\n        if port_ef:\n            self.ef_controller.port = port_ef\n            self.statusBar().showMessage(\"EF\u30ec\u30f3\u30ba\u81ea\u52d5\u63a5\u7d9a\u8a66\u884c\u4e2d...\")\n            try:\n                if self.ef_controller.connect():\n                    self._update_ef_ui_state(True)\n                    self.btn_ef_connect.setText(\"\u63a5\u7d9a\u6e08\")\n                    self.btn_ef_connect.setStyleSheet(\"background-color: #28a745; color: white;\")\n                    self.on_ef_refresh_position()\n            except Exception:\n                self.ef_controller.disconnect()\n\n        port_acuter = self.acuter_port_input.text().strip()\n        if port_acuter:\n            self.statusBar().showMessage(\"Acuter\u30de\u30a6\u30f3\u30c8\u81ea\u52d5\u63a5\u7d9a\u8a66\u884c\u4e2d...\")\n            try:\n                if self.acuter.connect(port_acuter):\n                    self.btn_acuter_connect.setText(\"\u5207\u65ad\")\n                    self.btn_acuter_connect.setStyleSheet(\"background-color: #28a745; color: white;\")\n                    self.lbl_acuter_status.setText(\"\u63a5\u7d9a\u6e08\u307f (GoTo\/Slew\u6709\u52b9)\")\n                    self.lbl_acuter_status.setStyleSheet(\"color: #00FF00; font-weight: bold;\")\n                    self._set_acuter_controls_state(True)\n                    self.acuter_poll_timer.start(500)\n            except Exception:\n                self.statusBar().showMessage(\"\u81ea\u52d5\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002\u5f8c\u3067\u624b\u52d5\u63a5\u7d9a\u3057\u3066\u304f\u3060\u3055\u3044\u3002\")\n\n    def _start_capture(self):\n        if self.capture_thread and self.capture_thread.isRunning():\n            self.capture_thread.stop()\n        \n        self.capture_thread = CaptureThread(self.camera, YOLO_MODEL_FILE)\n        self.capture_thread.target_classes = self.get_current_target_classes()\n        self.capture_thread.detection_enabled = self.cb_detection.isChecked()\n        self.capture_thread.motion_enabled = self.cb_motion.isChecked()\n        self.capture_thread.frame_ready.connect(self._on_frame)\n        self.capture_thread.error.connect(lambda msg: self.statusBar().showMessage(f\"\u53d6\u5f97\u30a8\u30e9\u30fc: {msg}\"))\n        self.capture_thread.start()\n\n    def _build_ui(self):\n        central = QWidget()\n        self.setCentralWidget(central)\n        root = QHBoxLayout(central)\n        root.setContentsMargins(15, 15, 15, 15)\n        root.setSpacing(15)\n\n        # ========== \u5de6\u30da\u30a4\u30f3 ==========\n        left_widget = QWidget()\n        left_layout = QVBoxLayout(left_widget)\n        left_layout.setContentsMargins(0, 0, 0, 0)\n        left_layout.setSpacing(15)\n\n        self.preview_label = VideoLabel('starting...')\n        self.preview_label.setMinimumSize(640, 480)\n        self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)\n        self.preview_label.setStyleSheet(\"background-color: #000000; color: #FFFFFF; border-radius: 8px;\")\n        self.preview_label.clicked.connect(self._on_preview_clicked)\n        self.preview_label.roi_selected.connect(self._on_roi_dragged)\n        left_layout.addWidget(self.preview_label, stretch=1)\n\n        self.ef_group = QGroupBox(\"Canon EF Lens (Astromechanics)\")\n        ef_layout = QVBoxLayout(self.ef_group)\n        \n        ef_conn_layout = QHBoxLayout()\n        self.ef_port_edit = QLineEdit(\"\/dev\/tty.usbserial-AK06UIRD\")\n        ef_conn_layout.addWidget(QLabel(\"\u30dd\u30fc\u30c8:\"))\n        ef_conn_layout.addWidget(self.ef_port_edit, stretch=1)\n        self.btn_ef_connect = QPushButton(\"\u63a5\u7d9a\")\n        self.btn_ef_connect.clicked.connect(self.on_ef_connect)\n        self.btn_ef_disconnect = QPushButton(\"\u5207\u65ad\")\n        self.btn_ef_disconnect.clicked.connect(self.on_ef_disconnect)\n        ef_conn_layout.addWidget(self.btn_ef_connect)\n        ef_conn_layout.addWidget(self.btn_ef_disconnect)\n        ef_layout.addLayout(ef_conn_layout)\n\n        ef_pos_move_row = QHBoxLayout()\n        self.lbl_ef_position = QLabel(\"\u2014\")\n        self.lbl_ef_position.setStyleSheet(\"color: white; background-color: #333; padding: 4px; border-radius: 4px;\")\n        self.btn_ef_refresh = QPushButton(\"\u66f4\u65b0\")\n        self.btn_ef_refresh.clicked.connect(self.on_ef_refresh_position)\n        self.spin_ef_target = QSpinBox()\n        self.spin_ef_target.setRange(0, 32767)\n        self.spin_ef_target.setValue(5000)\n        self.spin_ef_target.setSingleStep(100)\n        self.btn_ef_goto = QPushButton(\"\u79fb\u52d5\")\n        self.btn_ef_goto.clicked.connect(self.on_ef_goto)\n        ef_pos_move_row.addWidget(QLabel(\"\u73fe\u5728\u4f4d\u7f6e:\"))\n        ef_pos_move_row.addWidget(self.lbl_ef_position)\n        ef_pos_move_row.addWidget(self.btn_ef_refresh)\n        ef_pos_move_row.addSpacing(20)\n        ef_pos_move_row.addWidget(QLabel(\"\u76ee\u6a19\u4f4d\u7f6e:\"))\n        ef_pos_move_row.addWidget(self.spin_ef_target)\n        ef_pos_move_row.addWidget(self.btn_ef_goto)\n        ef_layout.addLayout(ef_pos_move_row)\n\n        ef_rel_ap_row = QHBoxLayout()\n        self.spin_ef_rel = QSpinBox()\n        self.spin_ef_rel.setRange(1, 5000)\n        self.spin_ef_rel.setValue(100)\n        self.spin_ef_rel.setSingleStep(50)\n        self.btn_ef_in = QPushButton(\"\u2190 IN (\u2212)\")\n        self.btn_ef_in.clicked.connect(lambda: self.on_ef_relative(-1))\n        self.btn_ef_out = QPushButton(\"OUT (+) \u2192\")\n        self.btn_ef_out.clicked.connect(lambda: self.on_ef_relative(+1))\n        self.spin_ef_aperture = QSpinBox()\n        self.spin_ef_aperture.setRange(0, 30)\n        self.spin_ef_aperture.setValue(0)\n        self.btn_ef_aperture = QPushButton(\"\u8a2d\u5b9a\")\n        self.btn_ef_aperture.clicked.connect(self.on_ef_set_aperture)\n        ef_rel_ap_row.addWidget(QLabel(\"\u76f8\u5bfe:\"))\n        ef_rel_ap_row.addWidget(self.spin_ef_rel)\n        ef_rel_ap_row.addWidget(self.btn_ef_in)\n        ef_rel_ap_row.addWidget(self.btn_ef_out)\n        ef_rel_ap_row.addSpacing(20)\n        ef_rel_ap_row.addWidget(QLabel(\"\u7d5e\u308a(0=\u958b\u653e):\"))\n        ef_rel_ap_row.addWidget(self.spin_ef_aperture)\n        ef_rel_ap_row.addWidget(self.btn_ef_aperture)\n        ef_rel_ap_row.addStretch()\n        ef_layout.addLayout(ef_rel_ap_row)\n        left_layout.addWidget(self.ef_group)\n        root.addWidget(left_widget, stretch=3)\n\n        # ========== \u53f3\u30da\u30a4\u30f3 ==========\n        right_scroll = QScrollArea()\n        right_scroll.setWidgetResizable(True)\n        right_scroll.setMinimumWidth(430)\n        right_scroll.setFrameShape(QScrollArea.Shape.NoFrame)\n\n        right_panel = QWidget()\n        right_layout = QVBoxLayout(right_panel)\n        right_layout.setContentsMargins(0, 0, 10, 0)\n        \n        self.ai_group = QGroupBox(\"AI Object Tracking\")\n        ai_layout = QVBoxLayout(self.ai_group)\n        self.cb_detection = QCheckBox(\"YOLOv8 \u30c8\u30e9\u30c3\u30ad\u30f3\u30b0\u3092\u6709\u52b9\u306b\u3059\u308b (ID\u4fdd\u6301)\")\n        self.cb_detection.setChecked(False)\n        self.cb_detection.toggled.connect(self.on_detection_toggled)\n        ai_layout.addWidget(self.cb_detection)\n\n        self.cb_motion = QCheckBox(\"\u52d5\u4f53\u691c\u77e5 (Motion Detect) [YOLO\u8a8d\u8b58\u524d\u306e\u5c0f\u76ee\u6a19\u7528]\")\n        self.cb_motion.setChecked(True)\n        self.cb_motion.setStyleSheet(\"color: #ffcc00; font-weight: bold;\")\n        self.cb_motion.toggled.connect(self.on_motion_toggled)\n        ai_layout.addWidget(self.cb_motion)\n\n        info_label = QLabel(\"\u203b\u30af\u30ea\u30c3\u30af\u3067\u300c\u5bfe\u8c61\u30ed\u30c3\u30af\u30aa\u30f3\u300d\u3001\u30c9\u30e9\u30c3\u30b0\u3067\u300cROI\u5207\u308a\u51fa\u3057\u300d\")\n        info_label.setStyleSheet(\"color: #aaaaaa; font-size: 11px;\")\n        ai_layout.addWidget(info_label)\n\n        targets_layout = QHBoxLayout()\n        self.cb_person = QCheckBox(\"\u4eba\")\n        self.cb_bicycle = QCheckBox(\"\u81ea\u8ee2\u8eca\")\n        self.cb_car = QCheckBox(\"\u8eca\")\n        self.cb_airplane = QCheckBox(\"\u822a\u7a7a\u6a5f\")\n        self.cb_bird = QCheckBox(\"\u9ce5\")\n        for cb in [self.cb_person, self.cb_bicycle, self.cb_car, self.cb_airplane, self.cb_bird]:\n            cb.setChecked(True)\n            cb.toggled.connect(self.update_target_classes)\n            targets_layout.addWidget(cb)\n        ai_layout.addLayout(targets_layout)\n        \n        self.cb_auto_engage = QCheckBox(\"\u9078\u629e\u5bfe\u8c61\u306e\u81ea\u52d5\u691c\u77e5\uff06\u9332\u753b (Auto Engage)\")\n        self.cb_auto_engage.setChecked(False)\n        self.cb_auto_engage.setStyleSheet(\"color: #5c9eff; font-weight: bold;\")\n        ai_layout.addWidget(self.cb_auto_engage)\n        right_layout.addWidget(self.ai_group)\n\n        self.acuter_group = QGroupBox(\"Acuter Traverse Control\")\n        self.acuter_layout = QVBoxLayout(self.acuter_group)\n\n        acuter_conn_layout = QHBoxLayout()\n        self.acuter_port_input = QLineEdit(\"\/dev\/cu.usbmodem4E94509B34001\")\n        self.btn_acuter_connect = QPushButton(\"\u63a5\u7d9a\")\n        self.btn_acuter_connect.clicked.connect(self.toggle_acuter_connection)\n        acuter_conn_layout.addWidget(self.acuter_port_input)\n        acuter_conn_layout.addWidget(self.btn_acuter_connect)\n        self.acuter_layout.addLayout(acuter_conn_layout)\n\n        self.lbl_acuter_status = QLabel(\"\u672a\u63a5\u7d9a\")\n        self.lbl_acuter_status.setStyleSheet(\"color: red; font-weight: bold;\")\n        self.acuter_layout.addWidget(self.lbl_acuter_status)\n        \n        self.btn_auto_track = QPushButton(\"Auto Tracking (OFF \/ \u5f85\u6a5f)\")\n        self.btn_auto_track.setCheckable(True)\n        self.btn_auto_track.setFixedHeight(60)\n        self.btn_auto_track.setStyleSheet(\"font-size: 18px; font-weight: bold; background-color: #555555; color: white;\")\n        self.btn_auto_track.toggled.connect(self.on_auto_track_toggled)\n        self.acuter_layout.addWidget(self.btn_auto_track)\n\n        pos_layout = QHBoxLayout()\n        self.lbl_acuter_az = QLabel(\"Az : --.- \u00b0\")\n        self.lbl_acuter_az.setStyleSheet(\"font-family: 'Menlo', 'Consolas', 'Courier New'; font-size: 16px; font-weight: bold; color: white; background-color: #333; padding: 4px; border-radius: 4px;\")\n        self.lbl_acuter_alt = QLabel(\"Alt: --.- \u00b0\")\n        self.lbl_acuter_alt.setStyleSheet(\"font-family: 'Menlo', 'Consolas', 'Courier New'; font-size: 16px; font-weight: bold; color: white; background-color: #333; padding: 4px; border-radius: 4px;\")\n        pos_layout.addWidget(self.lbl_acuter_az)\n        pos_layout.addWidget(self.lbl_acuter_alt)\n        self.acuter_layout.addLayout(pos_layout)\n\n        speed_form = QFormLayout()\n        self.acuter_speed_combo = QComboBox()\n        self.acuter_speed_combo.addItems([\"1.0\", \"5.0\", \"10.0\", \"15.0\"])\n        self.acuter_speed_combo.setCurrentText(\"15.0\")\n        speed_form.addRow(\"\u6700\u5927\u56de\u8ee2\u901f\u5ea6(\u5ea6\/\u79d2):\", self.acuter_speed_combo)\n        self.acuter_layout.addLayout(speed_form)\n\n        goto_layout = QHBoxLayout()\n        self.acuter_az_input = QLineEdit(\"10.0\")\n        self.acuter_az_input.setMaximumWidth(50)\n        self.btn_az_goto = QPushButton(\"Az GoTo\")\n        self.btn_az_goto.setEnabled(False)\n        self.btn_az_goto.clicked.connect(lambda: self.acuter.start_move(1, float(self.acuter_az_input.text()), exact_goto=True))\n        self.acuter_alt_input = QLineEdit(\"10.0\")\n        self.acuter_alt_input.setMaximumWidth(50)\n        self.btn_alt_goto = QPushButton(\"Alt GoTo\")\n        self.btn_alt_goto.setEnabled(False)\n        self.btn_alt_goto.clicked.connect(lambda: self.acuter.start_move(2, float(self.acuter_alt_input.text()), exact_goto=True))\n        goto_layout.addWidget(QLabel(\"Az:\"))\n        goto_layout.addWidget(self.acuter_az_input)\n        goto_layout.addWidget(QLabel(\"\u00b0\"))\n        goto_layout.addWidget(self.btn_az_goto)\n        goto_layout.addSpacing(15)\n        goto_layout.addWidget(QLabel(\"Alt:\"))\n        goto_layout.addWidget(self.acuter_alt_input)\n        goto_layout.addWidget(QLabel(\"\u00b0\"))\n        goto_layout.addWidget(self.btn_alt_goto)\n        goto_layout.addStretch()\n        self.acuter_layout.addLayout(goto_layout)\n\n        dpad_layout = QGridLayout()\n        self.btn_up = QPushButton(\"\u25b2\")\n        self.btn_left = QPushButton(\"\u25c0\")\n        self.btn_stop_center = QPushButton(\"\u25a0 STOP\")\n        self.btn_stop_center.setStyleSheet(\"background-color: #d9534f; color: white; font-weight: bold;\")\n        self.btn_right = QPushButton(\"\u25b6\")\n        self.btn_down = QPushButton(\"\u25bc\")\n\n        huge_angle = 1000.0 \n        self.btn_up.pressed.connect(lambda: self.acuter.start_move(2, -huge_angle, speed_deg_sec=float(self.acuter_speed_combo.currentText())))\n        self.btn_down.pressed.connect(lambda: self.acuter.start_move(2, huge_angle, speed_deg_sec=float(self.acuter_speed_combo.currentText())))\n        self.btn_left.pressed.connect(lambda: self.acuter.start_move(1, -huge_angle, speed_deg_sec=float(self.acuter_speed_combo.currentText())))\n        self.btn_right.pressed.connect(lambda: self.acuter.start_move(1, huge_angle, speed_deg_sec=float(self.acuter_speed_combo.currentText())))\n        \n        self.btn_up.released.connect(lambda: self.acuter.stop_axis(2))\n        self.btn_down.released.connect(lambda: self.acuter.stop_axis(2))\n        self.btn_left.released.connect(lambda: self.acuter.stop_axis(1))\n        self.btn_right.released.connect(lambda: self.acuter.stop_axis(1))\n        self.btn_stop_center.clicked.connect(self.acuter.emergency_stop)\n\n        dpad_layout.addWidget(self.btn_up, 0, 1)\n        dpad_layout.addWidget(self.btn_left, 1, 0)\n        dpad_layout.addWidget(self.btn_stop_center, 1, 1)\n        dpad_layout.addWidget(self.btn_right, 1, 2)\n        dpad_layout.addWidget(self.btn_down, 2, 1)\n        self.acuter_layout.addLayout(dpad_layout)\n        right_layout.addWidget(self.acuter_group)\n\n        # 3. ZWO ASI \u30ab\u30e1\u30e9\u8a2d\u5b9a\n        asi_group = QGroupBox('ZWO ASI &amp; Tracking Settings')\n        asi_layout = QVBoxLayout(asi_group)\n\n        self.cam_tabs = QTabWidget()\n        self.tab_main = QWidget()\n        self.tab_adv = QWidget()\n        form_main = QFormLayout(self.tab_main)\n        form_adv = QFormLayout(self.tab_adv)\n\n        self.btn_auto_calib = QPushButton(\"Run Auto Calibration\")\n        self.btn_auto_calib.clicked.connect(self.start_auto_calib)\n        self.btn_auto_calib.setStyleSheet(\"background-color: #28a745; color: white; font-weight: bold; font-size: 16px; padding: 12px;\")\n        form_main.addRow(self.btn_auto_calib)\n\n        track_calib_group = QGroupBox(\"Tracking Settings &amp; Axis Mapping\")\n        track_calib_layout = QFormLayout(track_calib_group)\n\n        self.focal_spin = QDoubleSpinBox()\n        self.focal_spin.setRange(1.0, 2000.0)\n        self.focal_spin.setValue(55.0) \n        self.kp_spin = QDoubleSpinBox()\n        self.kp_spin.setRange(0.01, 5.0)\n        self.kp_spin.setSingleStep(0.1)\n        self.kp_spin.setValue(0.8)\n        self.deadband_spin = QSpinBox()\n        self.deadband_spin.setRange(5, 200)\n        self.deadband_spin.setValue(40)\n\n        self.combo_x_axis = QComboBox()\n        self.combo_x_axis.addItems([\"Az (Axis 1)\", \"Alt (Axis 2)\"])\n        self.combo_x_axis.setCurrentIndex(0) \n        self.combo_y_axis = QComboBox()\n        self.combo_y_axis.addItems([\"Alt (Axis 2)\", \"Az (Axis 1)\"])\n        self.combo_y_axis.setCurrentIndex(0)\n        self.cb_inv_x = QCheckBox(\"Invert X-Axis (Reverse)\")\n        self.cb_inv_y = QCheckBox(\"Invert Y-Axis (Reverse)\")\n        self.cb_inv_y.setChecked(True)\n\n        test_move_widget = QWidget()\n        test_move_grid = QGridLayout(test_move_widget)\n        self.btn_test_alt_p = QPushButton(\"Alt +1.0\u00b0\")\n        self.btn_test_alt_m = QPushButton(\"Alt -1.0\u00b0\")\n        self.btn_test_az_m = QPushButton(\"Az -1.0\u00b0\")\n        self.btn_test_az_p = QPushButton(\"Az +1.0\u00b0\")\n        self.btn_test_alt_p.clicked.connect(lambda: self.acuter.start_move(2, 1.0, exact_goto=True, speed_deg_sec=2.0))\n        self.btn_test_alt_m.clicked.connect(lambda: self.acuter.start_move(2, -1.0, exact_goto=True, speed_deg_sec=2.0))\n        self.btn_test_az_m.clicked.connect(lambda: self.acuter.start_move(1, -1.0, exact_goto=True, speed_deg_sec=2.0))\n        self.btn_test_az_p.clicked.connect(lambda: self.acuter.start_move(1, 1.0, exact_goto=True, speed_deg_sec=2.0))\n        \n        self.calib_buttons = [self.btn_test_alt_p, self.btn_test_alt_m, self.btn_test_az_m, self.btn_test_az_p, self.btn_auto_calib]\n        for btn in self.calib_buttons: btn.setEnabled(False)\n\n        test_move_grid.addWidget(self.btn_test_alt_p, 0, 1)\n        test_move_grid.addWidget(self.btn_test_az_m, 1, 0)\n        test_move_grid.addWidget(self.btn_test_alt_m, 1, 1)\n        test_move_grid.addWidget(self.btn_test_az_p, 1, 2)\n\n        track_calib_layout.addRow(\"Focal Length (mm):\", self.focal_spin)\n        track_calib_layout.addRow(\"Tracking Gain (Kp):\", self.kp_spin)\n        track_calib_layout.addRow(\"Deadband (px):\", self.deadband_spin)\n        track_calib_layout.addRow(\"Camera X-Axis:\", self.combo_x_axis)\n        track_calib_layout.addRow(\"\", self.cb_inv_x)\n        track_calib_layout.addRow(\"Camera Y-Axis:\", self.combo_y_axis)\n        track_calib_layout.addRow(\"\", self.cb_inv_y)\n        track_calib_layout.addRow(\"Test Move:\", test_move_widget)\n        form_adv.addRow(track_calib_group)\n\n        cam_prop = self.camera.get_camera_property()\n        bin_group = QGroupBox(\"Sensor Binning\")\n        bin_layout = QHBoxLayout(bin_group)\n        self.bin_combo = QComboBox()\n        for b in cam_prop['SupportedBins']:\n            if b != 0: self.bin_combo.addItem(f\"Bin {b}x{b}\", b)\n        self.bin_combo.setCurrentText(f\"Bin {self.current_bins}x{self.current_bins}\")\n        self.btn_apply_bin = QPushButton(\"Apply Binning\")\n        self.btn_apply_bin.clicked.connect(self.on_apply_binning)\n        bin_layout.addWidget(QLabel(\"Binning:\"))\n        bin_layout.addWidget(self.bin_combo)\n        bin_layout.addWidget(self.btn_apply_bin)\n\n        roi_group = QGroupBox(\"Sensor ROI (Resolution)\")\n        roi_layout = QGridLayout(roi_group)\n        \n        current_w = cam_prop['MaxWidth'] \/\/ self.current_bins\n        current_h = cam_prop['MaxHeight'] \/\/ self.current_bins\n        \n        self.roi_w_spin = QSpinBox()\n        self.roi_w_spin.setRange(8, current_w)\n        self.roi_w_spin.setSingleStep(8)\n        self.roi_w_spin.setValue(current_w)\n        self.roi_h_spin = QSpinBox()\n        self.roi_h_spin.setRange(2, current_h)\n        self.roi_h_spin.setSingleStep(2)\n        self.roi_h_spin.setValue(current_h)\n        self.btn_apply_roi = QPushButton(\"Apply ROI (Center)\")\n        self.btn_apply_roi.clicked.connect(self.on_apply_roi_center)\n        self.btn_reset_roi = QPushButton(\"Reset to Full Frame\")\n        self.btn_reset_roi.clicked.connect(self.on_reset_roi)\n        self.btn_reset_roi.setStyleSheet(\"background-color: #555555;\")\n        \n        roi_layout.addWidget(QLabel(\"Width:\"), 0, 0)\n        roi_layout.addWidget(self.roi_w_spin, 0, 1)\n        roi_layout.addWidget(QLabel(\"Height:\"), 1, 0)\n        roi_layout.addWidget(self.roi_h_spin, 1, 1)\n        roi_layout.addWidget(self.btn_apply_roi, 2, 0, 1, 2)\n        roi_layout.addWidget(self.btn_reset_roi, 3, 0, 1, 2)\n        \n        form_adv.addRow(roi_group)\n        form_adv.addRow(bin_group)\n\n        self.rows = []\n        for name, caps in sorted(self.camera.get_controls().items()):\n            row = ControlRow(self.camera, caps, lambda: self.statusBar().showMessage('ASI\u30d1\u30e9\u30e1\u30fc\u30bf\u66f4\u65b0', 1000))\n            self.rows.append(row)\n            form_adv.addRow(name, row)\n\n        self.cam_tabs.addTab(self.tab_main, \"Main Controls\")\n        self.cam_tabs.addTab(self.tab_adv, \"Advanced\")\n        asi_layout.addWidget(self.cam_tabs)\n        \n        right_layout.addWidget(asi_group)\n        right_layout.addStretch()\n\n        right_scroll.setWidget(right_panel)\n        root.addWidget(right_scroll, stretch=2)\n        self.setStatusBar(QStatusBar())\n\n    def _update_ef_ui_state(self, connected: bool):\n        self.btn_ef_connect.setEnabled(not connected)\n        self.btn_ef_disconnect.setEnabled(connected)\n        self.btn_ef_goto.setEnabled(connected)\n        self.btn_ef_in.setEnabled(connected)\n        self.btn_ef_out.setEnabled(connected)\n\n    def on_ef_connect(self):\n        try:\n            self.ef_controller.port = self.ef_port_edit.text()\n            if self.ef_controller.connect():\n                self._update_ef_ui_state(True)\n                self.on_ef_refresh_position()\n        except: pass\n\n    def on_ef_disconnect(self):\n        self.ef_controller.disconnect()\n        self._update_ef_ui_state(False)\n\n    def on_ef_refresh_position(self):\n        if not self.ef_controller.is_connected: return\n        pos = self.ef_controller.get_position()\n        if pos is not None:\n            self.lbl_ef_position.setText(str(pos))\n            self.spin_ef_target.setValue(pos)\n\n    def on_ef_goto(self):\n        if self.ef_controller.is_connected:\n            self.ef_controller.move_absolute(self.spin_ef_target.value())\n            QTimer.singleShot(1000, self.on_ef_refresh_position)\n\n    def on_ef_relative(self, direction):\n        if self.ef_controller.is_connected:\n            target = self.spin_ef_target.value() + (self.spin_ef_rel.value() * direction)\n            self.ef_controller.move_absolute(target)\n            QTimer.singleShot(1000, self.on_ef_refresh_position)\n\n    def on_ef_set_aperture(self):\n        if self.ef_controller.is_connected:\n            self.ef_controller.set_aperture(self.spin_ef_aperture.value())\n\n    def toggle_acuter_connection(self):\n        if self.acuter.is_connected:\n            self.acuter.disconnect()\n            self.btn_acuter_connect.setText(\"\u63a5\u7d9a\")\n            self._set_acuter_controls_state(False)\n        else:\n            if self.acuter.connect(self.acuter_port_input.text()):\n                self.btn_acuter_connect.setText(\"\u5207\u65ad\")\n                self._set_acuter_controls_state(True)\n                self.acuter_poll_timer.start(500)\n\n    def _set_acuter_controls_state(self, state):\n        self.btn_az_goto.setEnabled(state)\n        self.btn_alt_goto.setEnabled(state)\n        self.btn_up.setEnabled(state)\n        self.btn_down.setEnabled(state)\n        self.btn_left.setEnabled(state)\n        self.btn_right.setEnabled(state)\n        self.btn_stop_center.setEnabled(state)\n        for btn in self.calib_buttons: btn.setEnabled(state)\n\n    def poll_acuter_position(self):\n        self.acuter.poll_position()\n        d1, d2 = self.acuter.current_deg.get(1), self.acuter.current_deg.get(2)\n        if d1 is not None: self.lbl_acuter_az.setText(f\"Az : {d1:+8.3f} \u00b0\")\n        if d2 is not None: self.lbl_acuter_alt.setText(f\"Alt: {d2:+8.3f} \u00b0\")\n        \n        if self.capture_thread:\n            self.capture_thread.camera_is_moving = self.acuter.axis_moving.get(1, False) or self.acuter.axis_moving.get(2, False)\n\n    def on_auto_track_toggled(self, checked):\n        if checked:\n            self.btn_auto_track.setText(\"Tracking (ON)\")\n            self.btn_auto_track.setStyleSheet(\"background-color: #d9534f; color: white; font-size: 18px; font-weight: bold;\")\n        else:\n            self.btn_auto_track.setText(\"Tracking (OFF)\")\n            self.btn_auto_track.setStyleSheet(\"background-color: #555; color: white; font-size: 18px; font-weight: bold;\")\n            self.acuter.emergency_stop()\n            if getattr(self, 'is_recording_event', False):\n                self.stop_event_record_and_return()\n\n    def on_motion_toggled(self, checked):\n        if self.capture_thread:\n            self.capture_thread.motion_enabled = checked\n            if not checked:\n                self.capture_thread.cv2_tracker = None\n\n    def start_event_record_and_track(self):\n        self.home_deg = {1: self.acuter.current_deg.get(1), 2: self.acuter.current_deg.get(2)}\n        self.is_recording_event = True\n        self.video_buffer = []\n        if not self.btn_auto_track.isChecked(): self.btn_auto_track.setChecked(True)\n\n    def stop_event_record_and_return(self):\n        self.is_recording_event = False\n        if self.video_buffer:\n            save_dir = \"\/Users\/mars\/acuter\/videos\"\n            os.makedirs(save_dir, exist_ok=True)\n            filename = os.path.join(save_dir, time.strftime(\"track_%Y%m%d_%H%M%S.mp4\"))\n            \n            self.statusBar().showMessage(f\"\u52d5\u753b\u3092\u4fdd\u5b58\u4e2d... {filename}\", 5000)\n            self.video_saver = VideoSaveWorker(self.video_buffer, filename, 30.0)\n            self.video_saver.finished_ok.connect(lambda f: self.statusBar().showMessage(f\"\u4fdd\u5b58\u5b8c\u4e86: {f}\", 5000))\n            self.video_saver.error.connect(lambda err: self.statusBar().showMessage(f\"\u4fdd\u5b58\u30a8\u30e9\u30fc: {err}\", 5000))\n            self.video_saver.start()\n            self.video_buffer = []\n            \n        for axis in (1, 2):\n            h_deg = self.home_deg.get(axis)\n            c_deg = self.acuter.current_deg.get(axis)\n            if h_deg is not None and c_deg is not None:\n                diff = h_deg - c_deg\n                if diff &gt; 180: diff -= 360\n                elif diff &lt; -180: diff += 360\n                if abs(diff) &gt; 0.05:\n                    self.acuter.start_move(axis, diff, exact_goto=True, speed_deg_sec=15.0)\n\n    def get_current_target_classes(self):\n        targets = []\n        if self.cb_person.isChecked(): targets.append(0)\n        if self.cb_bicycle.isChecked(): targets.append(1)\n        if self.cb_car.isChecked(): targets.append(2)\n        if self.cb_airplane.isChecked(): targets.append(4)\n        if self.cb_bird.isChecked(): targets.append(14)\n        return targets\n\n    def update_target_classes(self):\n        if self.capture_thread:\n            self.capture_thread.target_classes = self.get_current_target_classes()\n\n    def on_detection_toggled(self, checked):\n        if self.capture_thread:\n            self.capture_thread.detection_enabled = checked\n            if not checked:\n                self.target_id = None\n                self.btn_auto_track.setChecked(False)\n\n    def _on_preview_clicked(self, lx, ly):\n        scale = min(self.preview_label.width() \/ self.last_frame_size[0], self.preview_label.height() \/ self.last_frame_size[1])\n        ox = (self.preview_label.width() - self.last_frame_size[0] * scale) \/ 2\n        oy = (self.preview_label.height() - self.last_frame_size[1] * scale) \/ 2\n        fx, fy = (lx - ox) \/ scale, (ly - oy) \/ scale\n\n        clicked_id = None\n        for d in self.current_detections:\n            if d[0] &lt;= fx &lt;= d[2] and d[1] &lt;= fy &lt;= d[3]:\n                clicked_id = d[6]\n                break\n        \n        if clicked_id is not None:\n            self.target_id = clicked_id\n            if self.capture_thread: self.capture_thread.yolo_target_locked = True\n            self.lost_counter = 0  \n            if not getattr(self, 'is_recording_event', False):\n                self.start_event_record_and_track()\n            if not self.btn_auto_track.isChecked():\n                self.btn_auto_track.setChecked(True)\n            return\n\n        if getattr(self, 'last_motion_box', None) is not None:\n            mx, my, mw, mh = self.last_motion_box\n            if mx &lt;= fx &lt;= mx+mw and my &lt;= fy &lt;= my+mh:\n                self.target_id = None\n                if self.capture_thread: self.capture_thread.yolo_target_locked = False\n                if not getattr(self, 'is_recording_event', False):\n                    self.start_event_record_and_track()\n                if not self.btn_auto_track.isChecked():\n                    self.btn_auto_track.setChecked(True)\n                return\n\n        self.target_id = None\n        if self.capture_thread: self.capture_thread.yolo_target_locked = False\n        self.cb_auto_engage.setChecked(False)\n        self.btn_auto_track.setChecked(False)\n        self.acuter.emergency_stop()\n\n    def _on_roi_dragged(self, lx, ly, lw, lh): pass \n\n    def on_reset_roi(self):\n        cam_prop = self.camera.get_camera_property()\n        max_w = cam_prop['MaxWidth'] \/\/ self.current_bins\n        max_h = cam_prop['MaxHeight'] \/\/ self.current_bins\n        self.camera.set_roi(start_x=0, start_y=0, width=max_w, height=max_h, bins=self.current_bins)\n        self._start_capture()\n        \n    def on_apply_binning(self):\n        b = self.bin_combo.currentData()\n        self.camera.set_roi(bins=b)\n        self.current_bins = b\n        self._start_capture()\n        \n    def on_apply_roi_center(self):\n        w, h = (self.roi_w_spin.value() \/\/ 8) * 8, (self.roi_h_spin.value() \/\/ 2) * 2\n        cam_prop = self.camera.get_camera_property()\n        sx = ((cam_prop['MaxWidth'] \/\/ self.current_bins - w) \/\/ 2 \/\/ 4) * 4\n        sy = ((cam_prop['MaxHeight'] \/\/ self.current_bins - h) \/\/ 2 \/\/ 2) * 2\n        self.camera.set_roi(start_x=sx, start_y=sy, width=w, height=h, bins=self.current_bins)\n        self._start_capture()\n\n    def _on_frame(self, frame: np.ndarray, detections: list, motion_box: object = None):\n        self._frame_count += 1\n        self.last_motion_box = motion_box\n        h, w = frame.shape[:2]\n        self.last_frame_size = (w, h)\n        self.current_detections = detections\n        self.latest_frame = frame.copy()\n        \n        draw_frame = frame.copy()\n        cx, cy = w \/\/ 2, h \/\/ 2\n        \n        db_px = self.deadband_spin.value()\n        cv2.circle(draw_frame, (cx, cy), db_px, (255, 255, 255), 1, cv2.LINE_AA)\n        cv2.line(draw_frame, (cx-20, cy), (cx+20, cy), (255,255,255), 1)\n        cv2.line(draw_frame, (cx, cy-20), (cx, cy+20), (255,255,255), 1)\n\n        motion_cx, motion_cy = None, None\n        if motion_box is not None:\n            mx, my, mw, mh = [int(v) for v in motion_box]\n            motion_cx, motion_cy = mx + mw\/\/2, my + mh\/\/2\n            cv2.rectangle(draw_frame, (mx, my), (mx+mw, my+mh), (0, 165, 255), 2)\n            cv2.putText(draw_frame, \"MOTION\", (mx, max(my-10, 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 165, 255), 2)\n\n        if self.target_id is None and motion_box is not None and detections:\n            for d in detections:\n                dcx, dcy = (d[0]+d[2])\/\/2, (d[1]+d[3])\/\/2\n                if np.hypot(dcx - motion_cx, dcy - motion_cy) &lt; 100:\n                    self.target_id = d[6]\n                    self.last_target_name = d[5]\n                    if self.capture_thread: self.capture_thread.yolo_target_locked = True\n                    self.statusBar().showMessage(f\"Upgraded to YOLO Target: ID {self.target_id}\", 3000)\n                    break\n\n        if self.cb_auto_engage.isChecked() and self.target_id is None and detections:\n            best_det = max(detections, key=lambda d: d[4])\n            if best_det[6] is not None:\n                self.target_id = best_det[6]\n                self.last_target_name = best_det[5]\n                self.last_target_pos = ((best_det[0]+best_det[2])\/\/2, (best_det[1]+best_det[3])\/\/2)\n                self.lost_counter = 0\n                if self.capture_thread: self.capture_thread.yolo_target_locked = True\n                self.start_event_record_and_track()\n\n        if self.cb_auto_engage.isChecked() and self.target_id is None and motion_box is not None:\n            if not getattr(self, 'is_recording_event', False):\n                self.start_event_record_and_track()\n\n        is_tracking_active = False\n        tcx, tcy = None, None\n        tracking_color = (0, 255, 0)\n\n        if self.target_id is not None:\n            target_found = False\n            for d in detections:\n                if self.target_id == d[6]:\n                    target_found = True\n                    self.last_target_pos = ((d[0]+d[2])\/\/2, (d[1]+d[3])\/\/2)\n                    break\n            \n            if not target_found and self.last_target_pos:\n                best_d = None\n                min_dist = float('inf')\n                for d in detections:\n                    if d[5] == self.last_target_name:\n                        dist = np.hypot((((d[0]+d[2])\/\/2) - self.last_target_pos[0]), (((d[1]+d[3])\/\/2) - self.last_target_pos[1]))\n                        if dist &lt; min_dist:\n                            min_dist = dist\n                            best_d = d\n                if best_d is not None:\n                    self.target_id = best_d[6]\n                    target_found = True\n                    self.lost_counter = 0\n\n            if target_found:\n                tcx, tcy = self.last_target_pos\n                is_tracking_active = True\n                tracking_color = (0, 0, 255)\n                self.lost_counter = 0\n            else:\n                self.lost_counter += 1\n                if self.last_target_pos:\n                    gx, gy = self.last_target_pos\n                    cv2.circle(draw_frame, (gx, gy), 15, (0,165,255), 2)\n                    cv2.putText(draw_frame, \"SEARCHING...\", (gx+20, gy), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0,165,255), 2)\n                \n                if self.lost_counter == 1:\n                    self.acuter.emergency_stop()\n                    self.debug_info_status = \"Status: Target Lost (Braking)\"\n                \n                if self.lost_counter &gt; 150:\n                    self.debug_info_status = \"Status: Target completely LOST! (Aborted)\"\n                    self.acuter.emergency_stop()\n                    self.btn_auto_track.setChecked(False)\n                    self.target_id = None\n                    if self.capture_thread: self.capture_thread.yolo_target_locked = False\n                    self.last_target_pos = None\n                    self.lost_counter = 0\n\n        elif motion_box is not None:\n            tcx, tcy = motion_cx, motion_cy\n            is_tracking_active = True\n            tracking_color = (0, 165, 255)\n\n        for d in detections:\n            tid = d[6]\n            is_target = (self.target_id is not None and tid == self.target_id)\n            color = (0,0,255) if is_target else (0,255,0)\n            thickness = 4 if is_target else 2\n            \n            cv2.rectangle(draw_frame, (d[0], d[1]), (d[2], d[3]), color, thickness)\n            \n            id_str = f\"ID:{tid} \" if tid is not None else \"\"\n            label_text = f\"{id_str}{d[5]} {d[4]*100:.1f}%\"\n            if is_target:\n                label_text = \"[LOCKED] \" + label_text\n            \n            cv2.putText(draw_frame, label_text, (d[0], max(d[1] - 10, 10)), cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)\n\n        if is_tracking_active and tcx is not None:\n            cv2.line(draw_frame, (int(tcx) - 15, int(tcy)), (int(tcx) + 15, int(tcy)), tracking_color, 2)\n            cv2.line(draw_frame, (int(tcx), int(tcy) - 15), (int(tcx), int(tcy) + 15), tracking_color, 2)\n            cv2.line(draw_frame, (int(tcx), int(tcy)), (cx, cy), (0, 255, 255), 1)\n            \n            if self.btn_auto_track.isChecked() and self.calib_state == 0:\n                self._update_tracking(tcx, tcy, w, h)\n\n        if getattr(self, 'is_recording_event', False):\n            cv2.circle(draw_frame, (40, 40), 12, (0, 0, 255), -1)\n            cv2.putText(draw_frame, \"REC\", (60, 48), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3)\n            self.video_buffer.append(draw_frame.copy())\n\n        cv2.putText(draw_frame, getattr(self, 'debug_info_dxdy', ''), (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)\n        cv2.putText(draw_frame, getattr(self, 'debug_info_azalt', ''), (10, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)\n        cv2.putText(draw_frame, getattr(self, 'debug_info_status', ''), (10, 150), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 255), 2)\n\n        qimg = QImage(draw_frame.data, w, h, 3*w, QImage.Format.Format_BGR888)\n        self.preview_label.setPixmap(QPixmap.fromImage(qimg).scaled(self.preview_label.size(), Qt.AspectRatioMode.KeepAspectRatio))\n\n    def _update_tracking(self, tcx, tcy, w, h):\n        if not self.acuter.is_connected or time.time() &lt; self.next_track_time: return\n        self.next_track_time = time.time() + 0.1 \n        \n        dx, dy = tcx - w\/2.0, tcy - h\/2.0\n        db = self.deadband_spin.value()\n        \n        if abs(dx) &lt; db and abs(dy) &lt; db:\n            if self.acuter.axis_moving.get(1) or self.acuter.axis_moving.get(2):\n                self.acuter.emergency_stop()\n            self.debug_info_status = f\"Status: Target Centered (&lt;{db}px)\"\n            return\n            \n        deg_per_px = (self.pixel_size * self.current_bins \/ self.focal_spin.value()) * (180.0 \/ np.pi)\n        vx = dx * deg_per_px * self.kp_spin.value() * (-1.0 if self.cb_inv_x.isChecked() else 1.0)\n        vy = dy * deg_per_px * self.kp_spin.value() * (-1.0 if self.cb_inv_y.isChecked() else 1.0)\n\n        az_deg = vx if self.combo_x_axis.currentIndex() == 0 else vy\n        alt_deg = vx if self.combo_x_axis.currentIndex() == 1 else vy\n\n        spd = float(self.acuter_speed_combo.currentText())\n        db_deg = db * deg_per_px * self.kp_spin.value()\n\n        self.debug_info_dxdy = f\"Target diff: dx={dx:.1f}px, dy={dy:.1f}px\"\n\n        p_gain = 0.5 \n        speed_az = max(0.1, min(spd, abs(az_deg) * p_gain))\n        speed_alt = max(0.1, min(spd, abs(alt_deg) * p_gain))\n\n        sent = False\n        \n        if abs(az_deg) &gt; db_deg:\n            self.acuter.start_move(1, az_deg, exact_goto=False, speed_deg_sec=speed_az)\n            sent = True\n        elif self.acuter.axis_moving.get(1):\n            self.acuter.stop_axis(1)\n\n        if abs(alt_deg) &gt; db_deg:\n            self.acuter.start_move(2, alt_deg, exact_goto=False, speed_deg_sec=speed_alt)\n            sent = True\n        elif self.acuter.axis_moving.get(2):\n            self.acuter.stop_axis(2)\n\n        if sent:\n            self.debug_info_azalt = f\"Speed: Az {speed_az:.1f} d\/s, Alt {speed_alt:.1f} d\/s\"\n            self.debug_info_status = \"Status: Continuous P-Control Tracking\"\n\n    def start_auto_calib(self):\n        if not self.acuter.is_connected or self.latest_frame is None: return\n        self.btn_auto_track.setChecked(False)\n        self.calib_state = 1\n        self.calib_timer.start(1000)\n\n    def stop_calib(self, msg):\n        self.calib_state = 0\n        self.calib_timer.stop()\n        self.statusBar().showMessage(msg, 5000)\n\n    def calib_step(self):\n        if self.latest_frame is None: return\n        gray = cv2.cvtColor(self.latest_frame, cv2.COLOR_BGR2GRAY)\n        if self.calib_state == 1:\n            self.calib_pts_prev = cv2.goodFeaturesToTrack(gray, maxCorners=100, qualityLevel=0.01, minDistance=30)\n            if self.calib_pts_prev is None: return self.stop_calib(\"Failed\")\n            self.calib_gray_prev = gray\n            self.acuter.start_move(1, 1.0, exact_goto=True, speed_deg_sec=2.0)\n            self.calib_wait = 3\n            self.calib_state = 2\n        elif self.calib_state == 2:\n            self.calib_wait -= 1\n            if self.calib_wait &lt;= 0:\n                pts_next, status, _ = cv2.calcOpticalFlowPyrLK(self.calib_gray_prev, gray, self.calib_pts_prev, None)\n                diff = pts_next[status == 1] - self.calib_pts_prev[status == 1]\n                self.calib_dx1, self.calib_dy1 = np.median(diff[:, 0]), np.median(diff[:, 1])\n                self.acuter.start_move(1, -1.0, exact_goto=True, speed_deg_sec=2.0)\n                self.calib_wait = 3; self.calib_state = 3\n        elif self.calib_state == 3:\n            self.calib_wait -= 1\n            if self.calib_wait &lt;= 0:\n                self.calib_pts_prev = cv2.goodFeaturesToTrack(gray, maxCorners=100, qualityLevel=0.01, minDistance=30)\n                self.calib_gray_prev = gray\n                self.acuter.start_move(2, 1.0, exact_goto=True, speed_deg_sec=2.0)\n                self.calib_wait = 3; self.calib_state = 4\n        elif self.calib_state == 4:\n            self.calib_wait -= 1\n            if self.calib_wait &lt;= 0:\n                pts_next, status, _ = cv2.calcOpticalFlowPyrLK(self.calib_gray_prev, gray, self.calib_pts_prev, None)\n                diff = pts_next[status == 1] - self.calib_pts_prev[status == 1]\n                self.calib_dx2, self.calib_dy2 = np.median(diff[:, 0]), np.median(diff[:, 1])\n                self.acuter.start_move(2, -1.0, exact_goto=True, speed_deg_sec=2.0)\n                self.calib_wait = 3; self.calib_state = 5\n        elif self.calib_state == 5:\n            self.calib_wait -= 1\n            if self.calib_wait &lt;= 0:\n                if abs(self.calib_dx1) &gt; abs(self.calib_dy1):\n                    self.combo_x_axis.setCurrentIndex(0); self.combo_y_axis.setCurrentIndex(0)\n                    self.cb_inv_x.setChecked(bool(self.calib_dx1 &gt; 0))\n                    self.cb_inv_y.setChecked(bool(self.calib_dy2 &gt; 0))\n                else:\n                    self.combo_x_axis.setCurrentIndex(1); self.combo_y_axis.setCurrentIndex(1)\n                    self.cb_inv_x.setChecked(bool(self.calib_dx2 &gt; 0))\n                    self.cb_inv_y.setChecked(bool(self.calib_dy1 &gt; 0))\n                self.stop_calib(\"Success\")\n\n    def closeEvent(self, event):\n        self.acuter.disconnect()\n        self.ef_controller.disconnect()\n        if self.capture_thread: self.capture_thread.stop()\n        self.camera.close()\n        super().closeEvent(event)\n\nif __name__ == '__main__':\n    app = QApplication(sys.argv)\n    dark_stylesheet = \"\"\"\n        QMainWindow { background-color: #2b2b2b; } QLabel { color: #e0e0e0; font-size: 13px; }\n        QGroupBox { color: #e0e0e0; border: 1px solid #555; border-radius: 6px; margin-top: 16px; padding-top: 15px; font-weight: bold; }\n        QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; left: 10px; padding: 0 5px; }\n        QTabWidget::pane { border: 1px solid #555; } QTabBar::tab { background: #3c3c3c; color: white; padding: 8px 12px; }\n        QTabBar::tab:selected { background: #5c9eff; color: black; font-weight: bold; }\n        QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox { background-color: #3c3c3c; color: white; border: 1px solid #555; padding: 4px; border-radius: 4px; }\n        QCheckBox { color: #e0e0e0; }\n        QPushButton { background-color: #3c3c3c; color: #ffffff; border: 1px solid #555; border-radius: 4px; padding: 6px; font-weight: bold; }\n        QPushButton:pressed { background-color: #5c9eff; color: #000; }\n    \"\"\"\n    app.setStyleSheet(dark_stylesheet) \n    window = MainWindow(\"\/Users\/mars\/acuter\/ASI_Camera_SDK\/ASI_linux_mac_SDK_V1.41\/lib\/mac_arm64\/libASICamera2.dylib\")\n    window.show()\n    sys.exit(app.exec())<\/code><\/pre><\/div>\n\n<\/div>\n\t\t<\/div>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>1. \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u6982\u8981 \u672c\u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u306f\u3001ZWO ASI\u30ab\u30e1\u30e9\u3001Astromechanics EF\u30ec\u30f3\u30ba\u30b3\u30f3\u30c8\u30ed\u30fc\u30e9\u30fc\u3001\u304a\u3088\u3073 Acuter Traverse \u30de\u30a6\u30f3\u30c8\u3092\u7d71\u5408\u5236\u5fa1\u3057\u3001AI\u3092\u7528\u3044\u305f\u9ad8\u5ea6\u306a\u81ea\u52d5\u8ffd\u5c3e\u3068\u81ea\u52d5\u9332\u753b\u3092 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[2],"tags":[],"class_list":["post-2346","post","type-post","status-publish","format-standard","hentry","category-camera"],"featured_image_src":null,"author_info":{"display_name":"mars","author_link":"https:\/\/rfsec.ddns.net\/db\/?author=1"},"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=\/wp\/v2\/posts\/2346","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2346"}],"version-history":[{"count":5,"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=\/wp\/v2\/posts\/2346\/revisions"}],"predecessor-version":[{"id":2355,"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=\/wp\/v2\/posts\/2346\/revisions\/2355"}],"wp:attachment":[{"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2346"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2346"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rfsec.ddns.net\/db\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2346"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}