camera_automation.py 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495
  1. """
  2. Camera Automation Module
  3. Provides GUI automation for controlling external camera applications.
  4. Uses pywinauto for Windows-based window automation and keyboard/mouse control.
  5. Supports:
  6. - 2nd Look (Multispectral Camera)
  7. - EOS Utility (DSLR Camera)
  8. - AnalyzIR (Thermal Camera) - planned
  9. """
  10. import os
  11. import sys
  12. import time
  13. import tempfile
  14. import shutil
  15. from pathlib import Path
  16. from abc import ABC, abstractmethod
  17. from typing import Optional, Tuple
  18. from datetime import datetime
  19. import logging
  20. # Configure logging
  21. logger = logging.getLogger(__name__)
  22. logger.setLevel(logging.DEBUG)
  23. try:
  24. from pywinauto import Application, findwindows
  25. from pywinauto.keyboard import send_keys
  26. PYWINAUTO_AVAILABLE = True
  27. except ImportError:
  28. PYWINAUTO_AVAILABLE = False
  29. logger.warning("pywinauto not available - GUI automation will be limited")
  30. try:
  31. import pyautogui
  32. PYAUTOGUI_AVAILABLE = True
  33. except ImportError:
  34. PYAUTOGUI_AVAILABLE = False
  35. logger.warning("pyautogui not available - fallback automation unavailable")
  36. class CameraAutomationError(Exception):
  37. """Raised when camera automation fails."""
  38. pass
  39. class CameraAutomation(ABC):
  40. """
  41. Abstract base class for camera application automation.
  42. All camera automation implementations should inherit from this class
  43. and implement the required methods.
  44. """
  45. def __init__(self, app_name: str, window_title: str = None):
  46. """
  47. Initialize camera automation.
  48. Args:
  49. app_name: Name of the application (e.g., "2nd Look", "EOS Utility")
  50. window_title: Optional window title to search for
  51. """
  52. self.app_name = app_name
  53. self.window_title = window_title or app_name
  54. self.app = None
  55. self.window = None
  56. self.last_capture_time = None
  57. logger.info(f"Initialized {self.__class__.__name__} for {app_name}")
  58. @abstractmethod
  59. def find_window(self) -> bool:
  60. """
  61. Find and connect to the application window.
  62. Returns:
  63. bool: True if window found and connected, False otherwise
  64. """
  65. pass
  66. @abstractmethod
  67. def is_window_open(self) -> bool:
  68. """
  69. Check if the application window is currently open and responsive.
  70. Returns:
  71. bool: True if window is open and responsive, False otherwise
  72. """
  73. pass
  74. @abstractmethod
  75. def capture(self, output_dir: str = None) -> Optional[str]:
  76. """
  77. Perform automated capture from the application.
  78. Args:
  79. output_dir: Directory to save captured file (optional)
  80. Returns:
  81. str: Path to captured file if successful, None otherwise
  82. """
  83. pass
  84. def get_last_image(self) -> Optional[str]:
  85. """
  86. Retrieve the path to the last captured image.
  87. Returns:
  88. str: Path to last image if available, None otherwise
  89. """
  90. # Default implementation - should be overridden by subclasses
  91. return None
  92. class SecondLookAutomation(CameraAutomation):
  93. """
  94. Automation for 2nd Look multispectral camera application.
  95. Handles:
  96. - Finding and connecting to 2nd Look window
  97. - Clicking Record button and Trigger Software
  98. - Monitoring for TIFF file creation in Recordings directory
  99. - Stopping the recording
  100. - Cleanup of temporary files
  101. Recording directory: C:\\Users\\[USERNAME]\\Documents\\IO Industries\\2ndLook\\Recordings\\
  102. File pattern: Recording_YYYY-MM-DD_HH_MM_SS\\TIFF\\Camera\\Image_XXXXXX.tif
  103. """
  104. # Configuration constants
  105. RECORDING_BASE_DIR = Path.home() / "Documents" / "IO Industries" / "2ndLook" / "Recordings"
  106. TRIGGER_WAIT_TIME = 2 # seconds to wait after trigger
  107. FILE_WATCH_TIMEOUT = 15 # seconds to wait for file creation
  108. STOP_WAIT_TIME = 1 # seconds to wait after clicking stop
  109. # Button coordinates (calibrate with calibrate_2ndlook_buttons.py if needed)
  110. RECORD_BUTTON_POS = (64, 1422) # Red circle at bottom left
  111. TRIGGER_SOFTWARE_POS = (125, 357) # Toolbar button
  112. STOP_BUTTON_POS = (319, 1422) # Bottom stop button
  113. def __init__(self, default_save_dir: str = None):
  114. """
  115. Initialize 2nd Look automation.
  116. Args:
  117. default_save_dir: Default directory where 2nd Look saves files
  118. """
  119. super().__init__("2nd Look", "2ndLook - Control")
  120. self.default_save_dir = Path(default_save_dir) if default_save_dir else None
  121. self.last_image_path = None
  122. self.temp_dir = Path(tempfile.gettempdir()) / "dudong_2ndlook"
  123. self.temp_dir.mkdir(exist_ok=True)
  124. logger.info(f"2nd Look temp directory: {self.temp_dir}")
  125. logger.debug(f"Button coordinates - Record: {self.RECORD_BUTTON_POS}, Trigger: {self.TRIGGER_SOFTWARE_POS}, Stop: {self.STOP_BUTTON_POS}")
  126. def find_window(self) -> bool:
  127. """
  128. Find and connect to 2nd Look window.
  129. Returns:
  130. bool: True if found, False otherwise
  131. """
  132. if not PYWINAUTO_AVAILABLE:
  133. logger.error("pywinauto not available - cannot find window")
  134. return False
  135. try:
  136. logger.info(f"Searching for '{self.window_title}' window...")
  137. # Try to find window by title
  138. windows = findwindows.find_windows(title_re=f".*{self.window_title}.*")
  139. if windows:
  140. logger.info(f"Found {len(windows)} window(s) matching '{self.window_title}'")
  141. window_handle = windows[0]
  142. # Try to connect to application using the window handle
  143. try:
  144. # Use the window handle directly with pywinauto
  145. self.app = Application(backend="uia").connect(handle=window_handle)
  146. self.window = self.app.window(handle=window_handle)
  147. logger.info(f"Connected to 2nd Look (handle: {window_handle})")
  148. return True
  149. except Exception as e:
  150. logger.warning(f"Could not connect via UIA backend: {e}")
  151. try:
  152. # Fallback: try using top_level_only
  153. self.app = Application(backend="uia").connect(top_level_only=False)
  154. logger.info("Connected to application using fallback method")
  155. return True
  156. except Exception as e2:
  157. logger.warning(f"Fallback connection also failed: {e2}")
  158. return False
  159. else:
  160. logger.warning(f"No window found matching '{self.window_title}'")
  161. return False
  162. except Exception as e:
  163. logger.error(f"Error finding 2nd Look window: {e}")
  164. return False
  165. def is_window_open(self) -> bool:
  166. """
  167. Check if 2nd Look is open and responsive.
  168. Returns:
  169. bool: True if open and responsive, False otherwise
  170. """
  171. try:
  172. if not PYWINAUTO_AVAILABLE:
  173. logger.debug("pywinauto not available - cannot check window state")
  174. return False
  175. # Check for window existence
  176. windows = findwindows.find_windows(title_re=f".*{self.window_title}.*")
  177. if windows:
  178. logger.debug("2nd Look window is open")
  179. return True
  180. else:
  181. logger.debug("2nd Look window not found")
  182. return False
  183. except Exception as e:
  184. logger.warning(f"Error checking window state: {e}")
  185. return False
  186. def capture(self, output_dir: str = None) -> Optional[str]:
  187. """
  188. Capture multispectral image from 2nd Look using Record and Trigger Software.
  189. Workflow:
  190. 1. Click Record button (red circle)
  191. 2. Click Trigger Software button
  192. 3. Wait for TIFF file in Recordings directory
  193. 4. Click Stop button
  194. 5. Copy captured file to output directory
  195. Args:
  196. output_dir: Directory to save TIFF (uses temp dir if not specified)
  197. Returns:
  198. str: Path to captured TIFF file, or None if capture failed
  199. """
  200. output_dir = Path(output_dir) if output_dir else self.temp_dir
  201. output_dir.mkdir(parents=True, exist_ok=True)
  202. try:
  203. logger.info("Starting 2nd Look capture workflow...")
  204. # Step 1: Verify window is open
  205. if not self.is_window_open():
  206. logger.error("2nd Look window is not open")
  207. raise CameraAutomationError("2nd Look application not found or not running")
  208. # Step 2: Reconnect to window if needed
  209. if self.app is None or self.window is None:
  210. if not self.find_window():
  211. raise CameraAutomationError("Could not connect to 2nd Look window")
  212. # Step 3: Bring window to focus
  213. try:
  214. logger.debug("Bringing 2nd Look to focus...")
  215. self.window.set_focus()
  216. time.sleep(0.5)
  217. except Exception as e:
  218. logger.warning(f"Could not set focus: {e}")
  219. # Step 4: Click Record button (red circle at bottom left)
  220. logger.info("Clicking Record button...")
  221. self._click_record_button()
  222. time.sleep(1)
  223. # Step 5: Click Trigger Software button
  224. logger.info("Clicking Trigger Software button...")
  225. self._click_trigger_software_button()
  226. time.sleep(self.TRIGGER_WAIT_TIME)
  227. # Step 6: Wait for file creation in Recordings directory
  228. logger.info(f"Waiting for TIFF file in {self.RECORDING_BASE_DIR}...")
  229. captured_file = self._wait_for_recording_file()
  230. if not captured_file:
  231. logger.error("Timeout waiting for TIFF file creation")
  232. raise CameraAutomationError("Timeout waiting for file - capture may have failed")
  233. logger.info(f"File detected: {captured_file}")
  234. # Step 7: Click Stop button to end recording
  235. logger.info("Clicking Stop button...")
  236. self._click_stop_button()
  237. time.sleep(self.STOP_WAIT_TIME)
  238. # Step 8: Copy captured file to output directory
  239. logger.info(f"Copying file to output directory: {output_dir}")
  240. output_file = self._copy_captured_file(captured_file, output_dir)
  241. if output_file:
  242. logger.info(f"Successfully captured: {output_file}")
  243. self.last_image_path = str(output_file)
  244. self.last_capture_time = datetime.now()
  245. return str(output_file)
  246. else:
  247. logger.error("Failed to copy captured file")
  248. raise CameraAutomationError("Failed to copy captured file to output directory")
  249. except Exception as e:
  250. logger.error(f"Capture failed: {e}")
  251. return None
  252. def get_last_image(self) -> Optional[str]:
  253. """
  254. Get the path to the last captured image.
  255. Returns:
  256. str: Path to last image if available, None otherwise
  257. """
  258. if self.last_image_path and Path(self.last_image_path).exists():
  259. logger.debug(f"Returning last image: {self.last_image_path}")
  260. return self.last_image_path
  261. logger.debug("No valid last image path found")
  262. return None
  263. def set_button_coordinates(self, record_pos: Tuple[int, int] = None,
  264. trigger_pos: Tuple[int, int] = None,
  265. stop_pos: Tuple[int, int] = None) -> None:
  266. """
  267. Set button coordinates (for calibration).
  268. Args:
  269. record_pos: (x, y) coordinates for Record button
  270. trigger_pos: (x, y) coordinates for Trigger Software button
  271. stop_pos: (x, y) coordinates for Stop button
  272. """
  273. if record_pos:
  274. self.RECORD_BUTTON_POS = record_pos
  275. logger.info(f"Record button coordinates set to {record_pos}")
  276. if trigger_pos:
  277. self.TRIGGER_SOFTWARE_POS = trigger_pos
  278. logger.info(f"Trigger Software button coordinates set to {trigger_pos}")
  279. if stop_pos:
  280. self.STOP_BUTTON_POS = stop_pos
  281. logger.info(f"Stop button coordinates set to {stop_pos}")
  282. def _click_record_button(self) -> bool:
  283. """
  284. Click the Record button (red circle at bottom left).
  285. Uses coordinates from RECORD_BUTTON_POS (can be calibrated).
  286. Returns:
  287. bool: True if click was attempted, False otherwise
  288. """
  289. try:
  290. if not PYAUTOGUI_AVAILABLE:
  291. logger.warning("pyautogui not available - cannot click buttons")
  292. return False
  293. record_x, record_y = self.RECORD_BUTTON_POS
  294. logger.debug(f"Clicking Record button at ({record_x}, {record_y})")
  295. pyautogui.click(record_x, record_y)
  296. return True
  297. except Exception as e:
  298. logger.error(f"Error clicking Record button: {e}")
  299. return False
  300. def _click_trigger_software_button(self) -> bool:
  301. """
  302. Click the Trigger Software button in the toolbar.
  303. Uses coordinates from TRIGGER_SOFTWARE_POS (can be calibrated).
  304. Returns:
  305. bool: True if click was attempted, False otherwise
  306. """
  307. try:
  308. if not PYAUTOGUI_AVAILABLE:
  309. logger.warning("pyautogui not available - cannot click buttons")
  310. return False
  311. trigger_x, trigger_y = self.TRIGGER_SOFTWARE_POS
  312. logger.debug(f"Clicking Trigger Software button at ({trigger_x}, {trigger_y})")
  313. pyautogui.click(trigger_x, trigger_y)
  314. return True
  315. except Exception as e:
  316. logger.error(f"Error clicking Trigger Software button: {e}")
  317. return False
  318. def _click_stop_button(self) -> bool:
  319. """
  320. Click the Stop button (next to pause at bottom).
  321. Uses coordinates from STOP_BUTTON_POS (can be calibrated).
  322. Returns:
  323. bool: True if click was attempted, False otherwise
  324. """
  325. try:
  326. if not PYAUTOGUI_AVAILABLE:
  327. logger.warning("pyautogui not available - cannot click buttons")
  328. return False
  329. stop_x, stop_y = self.STOP_BUTTON_POS
  330. logger.debug(f"Clicking Stop button at ({stop_x}, {stop_y})")
  331. pyautogui.click(stop_x, stop_y)
  332. return True
  333. except Exception as e:
  334. logger.error(f"Error clicking Stop button: {e}")
  335. return False
  336. def _wait_for_recording_file(self, timeout: int = None) -> Optional[Path]:
  337. """
  338. Wait for a TIFF file to be created in the 2nd Look Recordings directory.
  339. File pattern: Recording_YYYY-MM-DD_HH_MM_SS/TIFF/Camera/Image_XXXXXX.tif
  340. Args:
  341. timeout: Maximum time to wait in seconds
  342. Returns:
  343. Path: Path to created TIFF file, or None if timeout
  344. """
  345. timeout = timeout or self.FILE_WATCH_TIMEOUT
  346. start_time = time.time()
  347. if not self.RECORDING_BASE_DIR.exists():
  348. logger.error(f"Recording directory not found: {self.RECORDING_BASE_DIR}")
  349. return None
  350. logger.debug(f"Monitoring for TIFF files in: {self.RECORDING_BASE_DIR}")
  351. while time.time() - start_time < timeout:
  352. try:
  353. # Search for TIFF files in subdirectories
  354. # Pattern: Recording_*/TIFF/Camera/*.tif
  355. tiff_files = list(self.RECORDING_BASE_DIR.glob("*/TIFF/Camera/*.tif"))
  356. if tiff_files:
  357. # Return the most recently created file
  358. newest_file = max(tiff_files, key=lambda p: p.stat().st_mtime)
  359. logger.info(f"Found TIFF file: {newest_file}")
  360. return newest_file
  361. time.sleep(0.5)
  362. except Exception as e:
  363. logger.warning(f"Error monitoring recording directory: {e}")
  364. time.sleep(1)
  365. logger.warning(f"No TIFF file created in {self.RECORDING_BASE_DIR} within {timeout} seconds")
  366. return None
  367. def _copy_captured_file(self, source_file: Path, output_dir: Path) -> Optional[Path]:
  368. """
  369. Copy captured TIFF file to output directory.
  370. Args:
  371. source_file: Path to source TIFF file
  372. output_dir: Directory to copy file to
  373. Returns:
  374. Path: Path to copied file, or None if copy failed
  375. """
  376. try:
  377. if not source_file.exists():
  378. logger.error(f"Source file not found: {source_file}")
  379. return None
  380. # Create output filename with timestamp
  381. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  382. output_filename = f"2ndlook_capture_{timestamp}.tif"
  383. output_file = output_dir / output_filename
  384. logger.debug(f"Copying {source_file} to {output_file}")
  385. shutil.copy2(source_file, output_file)
  386. logger.info(f"File copied successfully: {output_file}")
  387. return output_file
  388. except Exception as e:
  389. logger.error(f"Error copying file: {e}")
  390. return None
  391. def set_default_save_directory(self, directory: str) -> None:
  392. """
  393. Set the default save directory for 2nd Look exports.
  394. Args:
  395. directory: Path to save directory
  396. """
  397. self.default_save_dir = Path(directory)
  398. logger.info(f"Default save directory set to: {self.default_save_dir}")
  399. def cleanup(self) -> None:
  400. """Clean up resources and temporary files."""
  401. try:
  402. if self.window:
  403. try:
  404. # Try to close the app gracefully
  405. logger.debug("Closing 2nd Look window...")
  406. self.window.close()
  407. except Exception as e:
  408. logger.debug(f"Could not close window: {e}")
  409. # Clean up old temp files (older than 1 hour)
  410. if self.temp_dir.exists():
  411. now = time.time()
  412. for tiff_file in self.temp_dir.glob("*.tif*"):
  413. if now - tiff_file.stat().st_mtime > 3600: # 1 hour
  414. try:
  415. tiff_file.unlink()
  416. logger.debug(f"Cleaned up old file: {tiff_file.name}")
  417. except Exception as e:
  418. logger.warning(f"Could not delete {tiff_file}: {e}")
  419. logger.info("Cleanup completed")
  420. except Exception as e:
  421. logger.warning(f"Error during cleanup: {e}")
  422. class EOSUtilityAutomation(CameraAutomation):
  423. """
  424. Automation for EOS Utility DSLR camera application.
  425. Handles:
  426. - Finding and connecting to EOS Utility window
  427. - Clicking the capture button (X: 318, Y: 134)
  428. - Monitoring for JPG file creation in Pictures\\EOS-Utility directory
  429. - Copying captured file to output directory
  430. Pictures directory structure: C:\\Users\\[USERNAME]\\Pictures\\EOS-Utility\\[DATE]\\IMG_[SEQUENCE].JPG
  431. Example: C:\\Users\\AIDurian\\Pictures\\EOS-Utility\\2025_12_04\\IMG_0001.JPG
  432. """
  433. # Configuration constants
  434. PICTURES_BASE_DIR = Path.home() / "Pictures" / "EOS-Utility"
  435. CAPTURE_BUTTON_POS = (318, 134) # Click position for capture button
  436. FILE_WATCH_TIMEOUT = 10 # seconds to wait for file creation
  437. def __init__(self, default_save_dir: str = None):
  438. """
  439. Initialize EOS Utility automation.
  440. Args:
  441. default_save_dir: Default directory where captures should be saved
  442. """
  443. super().__init__("EOS Utility", "EOS M50m2")
  444. self.default_save_dir = Path(default_save_dir) if default_save_dir else None
  445. self.last_image_path = None
  446. self.temp_dir = Path(tempfile.gettempdir()) / "dudong_eos"
  447. self.temp_dir.mkdir(exist_ok=True)
  448. logger.info(f"EOS Utility temp directory: {self.temp_dir}")
  449. logger.debug(f"Capture button coordinates: {self.CAPTURE_BUTTON_POS}")
  450. def find_window(self) -> bool:
  451. """
  452. Find and connect to EOS Utility window.
  453. Returns:
  454. bool: True if found, False otherwise
  455. """
  456. if not PYWINAUTO_AVAILABLE:
  457. logger.error("pywinauto not available - cannot find window")
  458. return False
  459. try:
  460. logger.info(f"Searching for '{self.window_title}' window...")
  461. # Try to find window by title
  462. windows = findwindows.find_windows(title_re=f".*{self.window_title}.*")
  463. if windows:
  464. logger.info(f"Found {len(windows)} window(s) matching '{self.window_title}'")
  465. window_handle = windows[0]
  466. try:
  467. self.app = Application(backend="uia").connect(handle=window_handle)
  468. self.window = self.app.window(handle=window_handle)
  469. logger.info(f"Connected to EOS Utility (handle: {window_handle})")
  470. return True
  471. except Exception as e:
  472. logger.warning(f"Could not connect via UIA backend: {e}")
  473. try:
  474. self.app = Application(backend="uia").connect(top_level_only=False)
  475. logger.info("Connected to application using fallback method")
  476. return True
  477. except Exception as e2:
  478. logger.warning(f"Fallback connection also failed: {e2}")
  479. return False
  480. else:
  481. logger.warning(f"No window found matching '{self.window_title}'")
  482. return False
  483. except Exception as e:
  484. logger.error(f"Error finding EOS Utility window: {e}")
  485. return False
  486. def is_window_open(self) -> bool:
  487. """
  488. Check if EOS Utility is open and responsive.
  489. Returns:
  490. bool: True if open and responsive, False otherwise
  491. """
  492. try:
  493. if not PYWINAUTO_AVAILABLE:
  494. logger.debug("pywinauto not available - cannot check window state")
  495. return False
  496. windows = findwindows.find_windows(title_re=f".*{self.window_title}.*")
  497. if windows:
  498. logger.debug("EOS Utility window is open")
  499. return True
  500. else:
  501. logger.debug("EOS Utility window not found")
  502. return False
  503. except Exception as e:
  504. logger.warning(f"Error checking window state: {e}")
  505. return False
  506. def capture(self, output_dir: str = None) -> Optional[str]:
  507. """
  508. Capture image from EOS Utility DSLR camera.
  509. Workflow:
  510. 1. Bring window to focus
  511. 2. Click capture button (X: 318, Y: 134)
  512. 3. Wait for JPG file in Pictures\\EOS-Utility\\[DATE] directory
  513. 4. Copy captured file to output directory
  514. Args:
  515. output_dir: Directory to save JPG (uses temp dir if not specified)
  516. Returns:
  517. str: Path to captured JPG file, or None if capture failed
  518. """
  519. output_dir = Path(output_dir) if output_dir else self.temp_dir
  520. output_dir.mkdir(parents=True, exist_ok=True)
  521. try:
  522. logger.info("Starting EOS Utility capture workflow...")
  523. # Step 1: Verify window is open
  524. if not self.is_window_open():
  525. logger.error("EOS Utility window is not open")
  526. raise CameraAutomationError("EOS Utility application not found or not running")
  527. # Step 2: Reconnect to window if needed
  528. if self.app is None or self.window is None:
  529. if not self.find_window():
  530. raise CameraAutomationError("Could not connect to EOS Utility window")
  531. # Step 3: Bring window to focus
  532. try:
  533. logger.debug("Bringing EOS Utility to focus...")
  534. self.window.set_focus()
  535. time.sleep(0.5)
  536. except Exception as e:
  537. logger.warning(f"Could not set focus: {e}")
  538. # Step 4: Get the latest file timestamp before capture
  539. latest_file_before = self._get_latest_capture_file()
  540. logger.debug(f"Latest file before capture: {latest_file_before}")
  541. # Step 5: Click capture button
  542. logger.info("Clicking capture button...")
  543. self._click_capture_button()
  544. time.sleep(1)
  545. # Step 6: Wait for new file creation
  546. logger.info(f"Waiting for JPG file in {self.PICTURES_BASE_DIR}...")
  547. captured_file = self._wait_for_capture_file(latest_file_before)
  548. if not captured_file:
  549. logger.error("Timeout waiting for JPG file creation")
  550. raise CameraAutomationError("Timeout waiting for file - capture may have failed")
  551. logger.info(f"File detected: {captured_file}")
  552. # Step 7: Copy captured file to output directory
  553. logger.info(f"Copying file to output directory: {output_dir}")
  554. output_file = self._copy_captured_file(captured_file, output_dir)
  555. if output_file:
  556. logger.info(f"Successfully captured: {output_file}")
  557. self.last_image_path = str(output_file)
  558. self.last_capture_time = datetime.now()
  559. return str(output_file)
  560. else:
  561. logger.error("Failed to copy captured file")
  562. raise CameraAutomationError("Failed to copy captured file to output directory")
  563. except Exception as e:
  564. logger.error(f"Capture failed: {e}")
  565. return None
  566. def get_last_image(self) -> Optional[str]:
  567. """
  568. Get the path to the last captured image.
  569. Returns:
  570. str: Path to last image if available, None otherwise
  571. """
  572. if self.last_image_path and Path(self.last_image_path).exists():
  573. logger.debug(f"Returning last image: {self.last_image_path}")
  574. return self.last_image_path
  575. logger.debug("No valid last image path found")
  576. return None
  577. def set_button_coordinates(self, capture_pos: Tuple[int, int] = None) -> None:
  578. """
  579. Set capture button coordinates (for calibration).
  580. Args:
  581. capture_pos: (x, y) coordinates for capture button
  582. """
  583. if capture_pos:
  584. self.CAPTURE_BUTTON_POS = capture_pos
  585. logger.info(f"Capture button coordinates set to {capture_pos}")
  586. def _click_capture_button(self) -> bool:
  587. """
  588. Click the capture button in EOS Utility.
  589. Uses coordinates from CAPTURE_BUTTON_POS (can be calibrated).
  590. Returns:
  591. bool: True if click was attempted, False otherwise
  592. """
  593. try:
  594. if not PYAUTOGUI_AVAILABLE:
  595. logger.warning("pyautogui not available - cannot click buttons")
  596. return False
  597. capture_x, capture_y = self.CAPTURE_BUTTON_POS
  598. logger.debug(f"Clicking capture button at ({capture_x}, {capture_y})")
  599. pyautogui.click(capture_x, capture_y)
  600. return True
  601. except Exception as e:
  602. logger.error(f"Error clicking capture button: {e}")
  603. return False
  604. def _get_latest_capture_file(self) -> Optional[Path]:
  605. """
  606. Get the latest JPG file in the EOS Utility Pictures directory.
  607. Returns:
  608. Path: Path to latest JPG file, or None if no files exist
  609. """
  610. try:
  611. if not self.PICTURES_BASE_DIR.exists():
  612. logger.debug(f"EOS Utility Pictures directory not found yet: {self.PICTURES_BASE_DIR}")
  613. return None
  614. # Search for JPG files recursively
  615. jpg_files = list(self.PICTURES_BASE_DIR.glob("**/IMG_*.JPG"))
  616. if jpg_files:
  617. latest = max(jpg_files, key=lambda p: p.stat().st_mtime)
  618. logger.debug(f"Found latest capture file: {latest}")
  619. return latest
  620. logger.debug("No capture files found in Pictures directory")
  621. return None
  622. except Exception as e:
  623. logger.warning(f"Error getting latest capture file: {e}")
  624. return None
  625. def _wait_for_capture_file(self, file_before: Optional[Path] = None, timeout: int = None) -> Optional[Path]:
  626. """
  627. Wait for a new JPG file to be created in the EOS Utility Pictures directory.
  628. File pattern: Pictures\\EOS-Utility\\[DATE]\\IMG_[SEQUENCE].JPG
  629. Args:
  630. file_before: Previous file to compare against (detect new file)
  631. timeout: Maximum time to wait in seconds
  632. Returns:
  633. Path: Path to newly created JPG file, or None if timeout
  634. """
  635. timeout = timeout or self.FILE_WATCH_TIMEOUT
  636. start_time = time.time()
  637. if not self.PICTURES_BASE_DIR.exists():
  638. logger.debug(f"Creating Pictures directory: {self.PICTURES_BASE_DIR}")
  639. return None
  640. logger.debug(f"Monitoring for JPG files in: {self.PICTURES_BASE_DIR}")
  641. while time.time() - start_time < timeout:
  642. try:
  643. # Search for JPG files
  644. jpg_files = list(self.PICTURES_BASE_DIR.glob("**/IMG_*.JPG"))
  645. if jpg_files:
  646. # Get the most recently created file
  647. newest_file = max(jpg_files, key=lambda p: p.stat().st_mtime)
  648. # If we had a previous file, ensure this is a new one
  649. if file_before is None or newest_file != file_before:
  650. logger.info(f"Found new JPG file: {newest_file}")
  651. return newest_file
  652. time.sleep(0.5)
  653. except Exception as e:
  654. logger.warning(f"Error monitoring Pictures directory: {e}")
  655. time.sleep(1)
  656. logger.warning(f"No new JPG file created in {self.PICTURES_BASE_DIR} within {timeout} seconds")
  657. return None
  658. def _copy_captured_file(self, source_file: Path, output_dir: Path) -> Optional[Path]:
  659. """
  660. Copy captured JPG file to output directory.
  661. Args:
  662. source_file: Path to source JPG file
  663. output_dir: Directory to copy file to
  664. Returns:
  665. Path: Path to copied file, or None if copy failed
  666. """
  667. try:
  668. if not source_file.exists():
  669. logger.error(f"Source file not found: {source_file}")
  670. return None
  671. # Create output filename with timestamp
  672. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  673. output_filename = f"eos_capture_{timestamp}.jpg"
  674. output_file = output_dir / output_filename
  675. logger.debug(f"Copying {source_file} to {output_file}")
  676. shutil.copy2(source_file, output_file)
  677. logger.info(f"File copied successfully: {output_file}")
  678. return output_file
  679. except Exception as e:
  680. logger.error(f"Error copying file: {e}")
  681. return None
  682. def set_default_save_directory(self, directory: str) -> None:
  683. """
  684. Set the default save directory for EOS Utility exports.
  685. Args:
  686. directory: Path to save directory
  687. """
  688. self.default_save_dir = Path(directory)
  689. logger.info(f"Default save directory set to: {self.default_save_dir}")
  690. def cleanup(self) -> None:
  691. """Clean up resources and temporary files."""
  692. try:
  693. if self.window:
  694. try:
  695. logger.debug("Closing EOS Utility window...")
  696. self.window.close()
  697. except Exception as e:
  698. logger.debug(f"Could not close window: {e}")
  699. # Clean up old temp files (older than 1 hour)
  700. if self.temp_dir.exists():
  701. now = time.time()
  702. for jpg_file in self.temp_dir.glob("*.jpg"):
  703. if now - jpg_file.stat().st_mtime > 3600: # 1 hour
  704. try:
  705. jpg_file.unlink()
  706. logger.debug(f"Cleaned up old file: {jpg_file.name}")
  707. except Exception as e:
  708. logger.warning(f"Could not delete {jpg_file}: {e}")
  709. logger.info("Cleanup completed")
  710. except Exception as e:
  711. logger.warning(f"Error during cleanup: {e}")
  712. class AnalyzIRAutomation(CameraAutomation):
  713. """
  714. Automation for AnalyzIR thermal camera application (FOTRIC 323).
  715. Handles:
  716. - Finding and connecting to IR Camera and AnalyzIR Venus windows
  717. - Taking snapshot in IR Camera window
  718. - Performing automated click sequence to export thermal data
  719. - Monitoring for CSV file creation in Pictures\\AnalyzIR directory
  720. - Copying captured CSV file to output directory
  721. Workflow:
  722. 1. Check IR Camera window (SN:0803009169)
  723. 2. Left-click (X: 2515, Y: 898) to take snapshot in IR Camera
  724. 3. Switch to AnalyzIR Venus window
  725. 4. Right-click (X: 567, Y: 311) on latest data (from snapshot)
  726. 5. Left-click menu option (X: 694, Y: 450)
  727. 6. Left-click (X: 2370, Y: 109) - export/menu option
  728. 7. Left-click (X: 963, Y: 396) - confirmation/export
  729. 8. Left-click (X: 1713, Y: 1296) - button
  730. 9. Left-click (X: 1424, Y: 896) - save location
  731. 10. Left-click (X: 2522, Y: 26) - confirm
  732. 11. Left-click (X: 1497, Y: 892) - close/finalize
  733. File structure: C:\\Users\\[USERNAME]\\Pictures\\AnalyzIR\\YYYY-MM-DD_HHMMSS_CSV\\YYYY-MM-DD_HHMMSS.csv
  734. Example: C:\\Users\\AIDurian\\Pictures\\AnalyzIR\\2025-12-04_155903_CSV\\2025-12-04_155903.csv
  735. """
  736. # Configuration constants
  737. PICTURES_BASE_DIR = Path.home() / "Pictures" / "AnalyzIR"
  738. FILE_WATCH_TIMEOUT = 20 # seconds to wait for file creation
  739. CLICK_DELAY = 0.3 # delay between clicks
  740. # IR Camera window detection
  741. IR_CAMERA_WINDOW_TITLE = "IR Camera(SN:0803009169)"
  742. ANALYZIR_VENUS_WINDOW_TITLE = "AnalyzIR Venus"
  743. # Click coordinates for IR Camera snapshot
  744. IR_CAMERA_SNAPSHOT_POS = (2515, 898) # Take snapshot button in IR Camera window
  745. # AnalyzIR Venus click sequence
  746. ANALYZIR_VENUS_CLICKS = [
  747. (567, 311, "right"), # Right-click on latest data
  748. (694, 450, "left"), # Click menu option
  749. (2370, 109, "left"), # Export/menu option
  750. (963, 396, "left"), # Confirmation/export button
  751. (1713, 1296, "left"), # Button
  752. (1424, 896, "left"), # Save location
  753. (2522, 26, "left"), # Confirm
  754. (1497, 892, "left"), # Close/finalize
  755. ]
  756. def __init__(self, default_save_dir: str = None):
  757. """
  758. Initialize AnalyzIR automation.
  759. Args:
  760. default_save_dir: Default directory where captures should be saved
  761. """
  762. super().__init__("AnalyzIR", self.ANALYZIR_VENUS_WINDOW_TITLE)
  763. self.default_save_dir = Path(default_save_dir) if default_save_dir else None
  764. self.last_csv_path = None
  765. self.temp_dir = Path(tempfile.gettempdir()) / "dudong_analyzir"
  766. self.temp_dir.mkdir(exist_ok=True)
  767. self.ir_camera_window = None
  768. logger.info(f"AnalyzIR temp directory: {self.temp_dir}")
  769. logger.debug(f"IR Camera window title: {self.IR_CAMERA_WINDOW_TITLE}")
  770. logger.debug(f"AnalyzIR Venus window title: {self.ANALYZIR_VENUS_WINDOW_TITLE}")
  771. def find_window(self) -> bool:
  772. """
  773. Find and connect to AnalyzIR Venus window.
  774. Returns:
  775. bool: True if found, False otherwise
  776. """
  777. if not PYWINAUTO_AVAILABLE:
  778. logger.error("pywinauto not available - cannot find window")
  779. return False
  780. try:
  781. logger.info(f"Searching for '{self.ANALYZIR_VENUS_WINDOW_TITLE}' window...")
  782. # Try to find AnalyzIR Venus window with multiple patterns
  783. windows = []
  784. try:
  785. # Try exact match first
  786. windows = findwindows.find_windows(title_re=f".*{self.ANALYZIR_VENUS_WINDOW_TITLE}.*")
  787. if not windows:
  788. # Try partial match
  789. windows = findwindows.find_windows(title_re=".*AnalyzIR.*")
  790. logger.debug("Using partial 'AnalyzIR' pattern match")
  791. except Exception as e:
  792. logger.warning(f"Error searching for windows: {e}")
  793. if windows:
  794. logger.info(f"Found {len(windows)} window(s) matching search criteria")
  795. # Find the best match - prioritize exact title match
  796. window_handle = None
  797. for i, win in enumerate(windows):
  798. try:
  799. app_temp = Application(backend="uia").connect(handle=win, timeout=2)
  800. window_temp = app_temp.window(handle=win)
  801. title = window_temp.window_text() if hasattr(window_temp, 'window_text') else "Unknown"
  802. logger.debug(f" Window {i}: Handle={win}, Title='{title}'")
  803. # Prioritize "AnalyzIR Venus" without extra suffixes/paths
  804. if "AnalyzIR Venus" in title and "File" not in title:
  805. window_handle = win
  806. logger.debug(f" -> Selected as best match (exact title)")
  807. break
  808. elif window_handle is None:
  809. # Keep as fallback if no exact match found
  810. window_handle = win
  811. except Exception as e:
  812. logger.debug(f" Window {i}: Handle={win}, (could not get title: {e})")
  813. if window_handle is None:
  814. window_handle = win # Use as fallback
  815. if window_handle is None:
  816. window_handle = windows[0] # Final fallback
  817. try:
  818. self.app = Application(backend="uia").connect(handle=window_handle)
  819. self.window = self.app.window(handle=window_handle)
  820. logger.info(f"Connected to AnalyzIR Venus (handle: {window_handle})")
  821. except Exception as e:
  822. logger.warning(f"Could not connect via UIA backend: {e}")
  823. try:
  824. self.app = Application(backend="uia").connect(top_level_only=False)
  825. logger.info("Connected to application using fallback method")
  826. except Exception as e2:
  827. logger.warning(f"Fallback connection also failed: {e2}")
  828. return False
  829. # Also try to find and store IR Camera window with multiple patterns
  830. logger.debug(f"Searching for IR Camera window...")
  831. ir_windows = []
  832. try:
  833. # Try exact match
  834. ir_windows = findwindows.find_windows(title_re=f".*{self.IR_CAMERA_WINDOW_TITLE}.*")
  835. if not ir_windows:
  836. # Try partial match - just "IR Camera"
  837. ir_windows = findwindows.find_windows(title_re=".*IR Camera.*")
  838. logger.debug("Using partial 'IR Camera' pattern match")
  839. except Exception as e:
  840. logger.debug(f"Error searching for IR Camera window: {e}")
  841. if ir_windows:
  842. self.ir_camera_window = ir_windows[0]
  843. try:
  844. app_temp = Application(backend="uia").connect(handle=self.ir_camera_window, timeout=2)
  845. window_temp = app_temp.window(handle=self.ir_camera_window)
  846. title = window_temp.window_text() if hasattr(window_temp, 'window_text') else "Unknown"
  847. logger.info(f"Found IR Camera window (handle: {self.ir_camera_window}, title: '{title}')")
  848. except Exception as e:
  849. logger.info(f"Found IR Camera window (handle: {self.ir_camera_window})")
  850. else:
  851. logger.debug("IR Camera window not found (it may not be open yet)")
  852. return True
  853. else:
  854. logger.warning(f"No window found matching AnalyzIR criteria")
  855. logger.debug("Make sure AnalyzIR Venus is open and visible")
  856. return False
  857. except Exception as e:
  858. logger.error(f"Error finding AnalyzIR window: {e}")
  859. import traceback
  860. logger.debug(traceback.format_exc())
  861. return False
  862. def is_window_open(self) -> bool:
  863. """
  864. Check if AnalyzIR Venus window is open and responsive.
  865. Returns:
  866. bool: True if open and responsive, False otherwise
  867. """
  868. try:
  869. if not PYWINAUTO_AVAILABLE:
  870. logger.debug("pywinauto not available - cannot check window state")
  871. return False
  872. windows = findwindows.find_windows(title_re=f".*{self.ANALYZIR_VENUS_WINDOW_TITLE}.*")
  873. if windows:
  874. logger.debug("AnalyzIR Venus window is open")
  875. return True
  876. else:
  877. logger.debug("AnalyzIR Venus window not found")
  878. return False
  879. except Exception as e:
  880. logger.warning(f"Error checking window state: {e}")
  881. return False
  882. def capture(self, output_dir: str = None) -> Optional[str]:
  883. """
  884. Capture thermal data from AnalyzIR and export as CSV.
  885. Workflow:
  886. 1. Find and focus IR Camera window
  887. 2. Click close button in IR Camera window
  888. 3. Find and focus AnalyzIR Venus window
  889. 4. Perform automated click sequence to export data
  890. 5. Wait for CSV file creation in Pictures\\AnalyzIR directory
  891. 6. Copy captured CSV file to output directory
  892. Args:
  893. output_dir: Directory to save CSV (uses temp dir if not specified)
  894. Returns:
  895. str: Path to captured CSV file, or None if capture failed
  896. """
  897. output_dir = Path(output_dir) if output_dir else self.temp_dir
  898. output_dir.mkdir(parents=True, exist_ok=True)
  899. try:
  900. logger.info("Starting AnalyzIR capture workflow...")
  901. # Step 1: Verify windows are open
  902. if not self.is_window_open():
  903. logger.error("AnalyzIR Venus window is not open")
  904. raise CameraAutomationError("AnalyzIR Venus application not found or not running")
  905. # Step 2: Reconnect to windows if needed
  906. if self.app is None or self.window is None:
  907. if not self.find_window():
  908. raise CameraAutomationError("Could not connect to AnalyzIR window")
  909. # Step 3: Take snapshot in IR Camera window
  910. self._take_ir_snapshot()
  911. time.sleep(2) # Wait for snapshot to be processed and data to reach AnalyzIR
  912. # Step 4: Bring AnalyzIR Venus to focus
  913. try:
  914. logger.debug("Bringing AnalyzIR Venus to focus...")
  915. self.window.set_focus()
  916. time.sleep(0.5)
  917. # Move mouse to the AnalyzIR window to ensure it's active
  918. if PYAUTOGUI_AVAILABLE:
  919. # Move to center of expected window area
  920. pyautogui.moveTo(1000, 400)
  921. time.sleep(0.2)
  922. except Exception as e:
  923. logger.warning(f"Could not set focus: {e}")
  924. # Step 5: Get the latest file timestamp before capture
  925. latest_file_before = self._get_latest_csv_file()
  926. logger.debug(f"Latest CSV file before capture: {latest_file_before}")
  927. # Step 6: Perform automated click sequence to export data
  928. logger.info("Performing automated click sequence to export thermal data...")
  929. self._perform_export_sequence()
  930. # Step 7: Wait for new CSV file creation
  931. logger.info(f"Waiting for CSV file in {self.PICTURES_BASE_DIR}...")
  932. captured_file = self._wait_for_csv_file(latest_file_before)
  933. if not captured_file:
  934. logger.error("Timeout waiting for CSV file creation")
  935. raise CameraAutomationError("Timeout waiting for CSV file - capture may have failed")
  936. logger.info(f"CSV file detected: {captured_file}")
  937. # Step 8: Copy captured file to output directory
  938. logger.info(f"Copying file to output directory: {output_dir}")
  939. output_file = self._copy_captured_file(captured_file, output_dir)
  940. if output_file:
  941. logger.info(f"Successfully captured: {output_file}")
  942. self.last_csv_path = str(output_file)
  943. self.last_capture_time = datetime.now()
  944. return str(output_file)
  945. else:
  946. logger.error("Failed to copy captured file")
  947. raise CameraAutomationError("Failed to copy captured file to output directory")
  948. except Exception as e:
  949. logger.error(f"Capture failed: {e}")
  950. return None
  951. def get_last_image(self) -> Optional[str]:
  952. """
  953. Get the path to the last captured CSV file.
  954. Returns:
  955. str: Path to last CSV file if available, None otherwise
  956. """
  957. if self.last_csv_path and Path(self.last_csv_path).exists():
  958. logger.debug(f"Returning last CSV: {self.last_csv_path}")
  959. return self.last_csv_path
  960. logger.debug("No valid last CSV path found")
  961. return None
  962. def _take_ir_snapshot(self) -> bool:
  963. """
  964. Take a snapshot in the IR Camera window.
  965. Steps:
  966. 1. Focus/activate IR Camera window
  967. 2. Click the snapshot button at coordinates (2515, 898)
  968. 3. Wait for snapshot to be processed
  969. Returns:
  970. bool: True if click was attempted, False otherwise
  971. """
  972. try:
  973. if not PYAUTOGUI_AVAILABLE:
  974. logger.warning("pyautogui not available - cannot click buttons")
  975. return False
  976. # Step 1: Try to focus IR Camera window if found
  977. if self.ir_camera_window:
  978. try:
  979. logger.debug(f"Focusing IR Camera window (handle: {self.ir_camera_window})...")
  980. ir_app = Application(backend="uia").connect(handle=self.ir_camera_window, timeout=2)
  981. ir_window = ir_app.window(handle=self.ir_camera_window)
  982. # Bring window to foreground and focus
  983. ir_window.set_focus()
  984. time.sleep(0.3)
  985. logger.info("IR Camera window focused")
  986. except Exception as e:
  987. logger.debug(f"Could not focus IR Camera window: {e}")
  988. # Continue anyway - click might still work
  989. else:
  990. logger.debug("IR Camera window handle not available - clicking anyway")
  991. # Step 2: Click snapshot button
  992. snapshot_x, snapshot_y = self.IR_CAMERA_SNAPSHOT_POS
  993. logger.debug(f"Clicking IR Camera snapshot button at ({snapshot_x}, {snapshot_y})")
  994. pyautogui.click(snapshot_x, snapshot_y)
  995. time.sleep(0.5) # Increased wait
  996. logger.info("Snapshot taken in IR Camera")
  997. return True
  998. except Exception as e:
  999. logger.warning(f"Could not take IR Camera snapshot (non-critical): {e}")
  1000. return False
  1001. def _perform_export_sequence(self) -> bool:
  1002. """
  1003. Perform the automated click sequence to export thermal data from AnalyzIR Venus.
  1004. Sequence:
  1005. 1. Right-click at (567, 311) on latest snapshot data
  1006. 2. Left-click menu option at (694, 450)
  1007. 3. Left-click export/menu at (2370, 109)
  1008. 4. Left-click confirmation at (963, 396)
  1009. 5. Left-click button at (1713, 1296)
  1010. 6. Left-click save location at (1424, 896)
  1011. 7. Left-click confirm at (2522, 26)
  1012. 8. Left-click close/finalize at (1497, 892)
  1013. Returns:
  1014. bool: True if sequence completed, False otherwise
  1015. """
  1016. try:
  1017. if not PYAUTOGUI_AVAILABLE:
  1018. logger.warning("pyautogui not available - cannot perform click sequence")
  1019. return False
  1020. for i, (x, y, click_type) in enumerate(self.ANALYZIR_VENUS_CLICKS, 1):
  1021. logger.debug(f"Click {i}/{len(self.ANALYZIR_VENUS_CLICKS)}: {click_type}-click at ({x}, {y})")
  1022. if click_type == "right":
  1023. pyautogui.click(x, y, button='right')
  1024. else: # left
  1025. pyautogui.click(x, y)
  1026. time.sleep(self.CLICK_DELAY)
  1027. logger.info("Export sequence completed successfully")
  1028. return True
  1029. except Exception as e:
  1030. logger.error(f"Error performing export sequence: {e}")
  1031. return False
  1032. def _get_latest_csv_file(self) -> Optional[Path]:
  1033. """
  1034. Get the latest CSV file in the AnalyzIR Pictures directory.
  1035. Returns:
  1036. Path: Path to latest CSV file, or None if no files exist
  1037. """
  1038. try:
  1039. if not self.PICTURES_BASE_DIR.exists():
  1040. logger.debug(f"AnalyzIR Pictures directory not found yet: {self.PICTURES_BASE_DIR}")
  1041. return None
  1042. # Search for CSV files in subdirectories (pattern: YYYY-MM-DD_HHMMSS_CSV/*.csv)
  1043. csv_files = list(self.PICTURES_BASE_DIR.glob("*_CSV/*.csv"))
  1044. if csv_files:
  1045. latest = max(csv_files, key=lambda p: p.stat().st_mtime)
  1046. logger.debug(f"Found latest CSV file: {latest}")
  1047. return latest
  1048. logger.debug("No CSV files found in Pictures directory")
  1049. return None
  1050. except Exception as e:
  1051. logger.warning(f"Error getting latest CSV file: {e}")
  1052. return None
  1053. def _wait_for_csv_file(self, file_before: Optional[Path] = None, timeout: int = None) -> Optional[Path]:
  1054. """
  1055. Wait for a new CSV file to be created in the AnalyzIR Pictures directory.
  1056. File pattern: Pictures\\AnalyzIR\\YYYY-MM-DD_HHMMSS_CSV\\YYYY-MM-DD_HHMMSS.csv
  1057. Args:
  1058. file_before: Previous file to compare against (detect new file)
  1059. timeout: Maximum time to wait in seconds
  1060. Returns:
  1061. Path: Path to newly created CSV file, or None if timeout
  1062. """
  1063. timeout = timeout or self.FILE_WATCH_TIMEOUT
  1064. start_time = time.time()
  1065. if not self.PICTURES_BASE_DIR.exists():
  1066. logger.debug(f"Creating Pictures/AnalyzIR directory: {self.PICTURES_BASE_DIR}")
  1067. self.PICTURES_BASE_DIR.mkdir(parents=True, exist_ok=True)
  1068. logger.debug(f"Monitoring for CSV files in: {self.PICTURES_BASE_DIR}")
  1069. while time.time() - start_time < timeout:
  1070. try:
  1071. # Search for CSV files in subdirectories
  1072. csv_files = list(self.PICTURES_BASE_DIR.glob("*_CSV/*.csv"))
  1073. if csv_files:
  1074. # Get the most recently created file
  1075. newest_file = max(csv_files, key=lambda p: p.stat().st_mtime)
  1076. # If we had a previous file, ensure this is a new one
  1077. if file_before is None or newest_file != file_before:
  1078. logger.info(f"Found new CSV file: {newest_file}")
  1079. return newest_file
  1080. time.sleep(0.5)
  1081. except Exception as e:
  1082. logger.warning(f"Error monitoring AnalyzIR directory: {e}")
  1083. time.sleep(1)
  1084. logger.warning(f"No new CSV file created in {self.PICTURES_BASE_DIR} within {timeout} seconds")
  1085. return None
  1086. def _copy_captured_file(self, source_file: Path, output_dir: Path) -> Optional[Path]:
  1087. """
  1088. Copy captured CSV file to output directory.
  1089. Args:
  1090. source_file: Path to source CSV file
  1091. output_dir: Directory to copy file to
  1092. Returns:
  1093. Path: Path to copied file, or None if copy failed
  1094. """
  1095. try:
  1096. if not source_file.exists():
  1097. logger.error(f"Source file not found: {source_file}")
  1098. return None
  1099. # Create output filename with timestamp
  1100. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  1101. output_filename = f"analyzir_thermal_{timestamp}.csv"
  1102. output_file = output_dir / output_filename
  1103. logger.debug(f"Copying {source_file} to {output_file}")
  1104. shutil.copy2(source_file, output_file)
  1105. logger.info(f"File copied successfully: {output_file}")
  1106. return output_file
  1107. except Exception as e:
  1108. logger.error(f"Error copying file: {e}")
  1109. return None
  1110. def set_default_save_directory(self, directory: str) -> None:
  1111. """
  1112. Set the default save directory for AnalyzIR exports.
  1113. Args:
  1114. directory: Path to save directory
  1115. """
  1116. self.default_save_dir = Path(directory)
  1117. logger.info(f"Default save directory set to: {self.default_save_dir}")
  1118. def cleanup(self) -> None:
  1119. """Clean up resources and temporary files."""
  1120. try:
  1121. if self.window:
  1122. try:
  1123. logger.debug("Closing AnalyzIR window...")
  1124. self.window.close()
  1125. except Exception as e:
  1126. logger.debug(f"Could not close window: {e}")
  1127. # Clean up old temp files (older than 1 hour)
  1128. if self.temp_dir.exists():
  1129. now = time.time()
  1130. for csv_file in self.temp_dir.glob("*.csv"):
  1131. if now - csv_file.stat().st_mtime > 3600: # 1 hour
  1132. try:
  1133. csv_file.unlink()
  1134. logger.debug(f"Cleaned up old file: {csv_file.name}")
  1135. except Exception as e:
  1136. logger.warning(f"Could not delete {csv_file}: {e}")
  1137. logger.info("Cleanup completed")
  1138. except Exception as e:
  1139. logger.warning(f"Error during cleanup: {e}")
  1140. # Debug utility function
  1141. def debug_analyzir_windows():
  1142. """
  1143. Debug function to find and display all open windows that match AnalyzIR patterns.
  1144. Useful for troubleshooting window detection issues.
  1145. """
  1146. if not PYWINAUTO_AVAILABLE:
  1147. print("pywinauto not available")
  1148. return
  1149. print("\n" + "=" * 70)
  1150. print("AnalyzIR Window Detection Debug")
  1151. print("=" * 70)
  1152. try:
  1153. # Find all windows
  1154. print("\nSearching for AnalyzIR-related windows...")
  1155. patterns = [
  1156. ("AnalyzIR Venus", ".*AnalyzIR Venus.*"),
  1157. ("AnalyzIR (any)", ".*AnalyzIR.*"),
  1158. ("IR Camera (exact)", ".*IR Camera\\(SN:0803009169\\).*"),
  1159. ("IR Camera (partial)", ".*IR Camera.*"),
  1160. ]
  1161. for pattern_name, pattern in patterns:
  1162. print(f"\nPattern: {pattern_name}")
  1163. print(f"Regex: {pattern}")
  1164. try:
  1165. windows = findwindows.find_windows(title_re=pattern)
  1166. if windows:
  1167. print(f" Found {len(windows)} window(s):")
  1168. for i, win_handle in enumerate(windows):
  1169. try:
  1170. app = Application(backend="uia").connect(handle=win_handle, timeout=1)
  1171. window = app.window(handle=win_handle)
  1172. title = window.window_text() if hasattr(window, 'window_text') else "Unknown"
  1173. print(f" {i+1}. Handle: {win_handle}, Title: '{title}'")
  1174. except Exception as e:
  1175. print(f" {i+1}. Handle: {win_handle}, (could not get title)")
  1176. else:
  1177. print(" No windows found")
  1178. except Exception as e:
  1179. print(f" Error: {e}")
  1180. print("\n" + "=" * 70)
  1181. except Exception as e:
  1182. print(f"Error during debug: {e}")
  1183. import traceback
  1184. traceback.print_exc()
  1185. # Factory function for creating camera automation instances
  1186. def create_camera_automation(camera_type: str, **kwargs) -> Optional[CameraAutomation]:
  1187. """
  1188. Factory function to create camera automation instances.
  1189. Args:
  1190. camera_type: Type of camera ("2nd_look", "eos_utility", "analyzir")
  1191. **kwargs: Additional arguments passed to the automation class
  1192. Returns:
  1193. CameraAutomation: Automation instance, or None if type not recognized
  1194. """
  1195. camera_type = camera_type.lower().strip()
  1196. if camera_type in ("2nd_look", "2ndlook", "multispectral"):
  1197. return SecondLookAutomation(**kwargs)
  1198. elif camera_type in ("eos_utility", "eosutility", "dslr"):
  1199. return EOSUtilityAutomation(**kwargs)
  1200. elif camera_type in ("analyzir", "thermal", "fotric"):
  1201. return AnalyzIRAutomation(**kwargs)
  1202. else:
  1203. logger.error(f"Unknown camera type: {camera_type}")
  1204. return None