Coverage for src/pytribeam/GUI/config_ui/microscope_interface.py: 0%

96 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2026-06-16 18:30 +0000

1"""Microscope communication interface. 

2 

3This module provides an abstraction layer for microscope communication, 

4separating hardware interaction from UI code. 

5""" 

6 

7from typing import Optional, Dict, Tuple 

8 

9import pytribeam.factory as factory 

10import pytribeam.laser as laser 

11import pytribeam.types as tbt 

12import pytribeam.utilities as ut 

13from pytribeam.GUI.common.errors import MicroscopeConnectionError 

14from pytribeam.constants import Conversions 

15 

16 

17class MicroscopeInterface: 

18 """Interface for microscope communication. 

19 

20 This class abstracts microscope operations and provides a clean API 

21 for the GUI to interact with hardware. It handles connection management 

22 and provides context manager support for automatic cleanup. 

23 

24 Attributes: 

25 host: Connection host address 

26 port: Connection port 

27 is_connected: Whether microscope is currently connected 

28 """ 

29 

30 def __init__(self, host: str = "localhost", port: str = ""): 

31 """Initialize microscope interface. 

32 

33 Args: 

34 host: Microscope connection host (default: 'localhost') 

35 port: Microscope connection port (default: '') 

36 """ 

37 self.host = host 

38 self.port = port 

39 self._microscope: Optional[tbt.Microscope] = None 

40 

41 @property 

42 def is_connected(self) -> bool: 

43 """Check if microscope is connected.""" 

44 return self._microscope is not None 

45 

46 @property 

47 def microscope(self) -> Optional[tbt.Microscope]: 

48 """Get microscope object (read-only access).""" 

49 return self._microscope 

50 

51 def __enter__(self): 

52 """Context manager entry - connect to microscope.""" 

53 self.connect() 

54 return self 

55 

56 def __exit__(self, exc_type, exc_val, exc_tb): 

57 """Context manager exit - disconnect from microscope.""" 

58 self.disconnect() 

59 return False 

60 

61 def connect(self): 

62 """Establish connection to microscope. 

63 

64 Raises: 

65 MicroscopeConnectionError: If connection fails 

66 """ 

67 if self.is_connected: 

68 return # Already connected 

69 

70 try: 

71 self._microscope = tbt.Microscope() 

72 ut.connect_microscope( 

73 self._microscope, 

74 quiet_output=True, 

75 connection_host=self.host, 

76 connection_port=self.port, 

77 ) 

78 except ConnectionError as e: 

79 self._microscope = None 

80 raise MicroscopeConnectionError( 

81 f"Failed to connect to microscope at {self.host}:{self.port}" 

82 ) from e 

83 

84 def disconnect(self): 

85 """Close microscope connection.""" 

86 if self._microscope is not None: 

87 try: 

88 ut.disconnect_microscope(self._microscope) 

89 except Exception: 

90 pass # Ignore errors during disconnect 

91 finally: 

92 self._microscope = None 

93 

94 def ensure_connected(self): 

95 """Ensure microscope is connected, raise if not. 

96 

97 Raises: 

98 MicroscopeConnectionError: If not connected 

99 """ 

100 if not self.is_connected: 

101 raise MicroscopeConnectionError( 

102 "Not connected to microscope. Call connect() first." 

103 ) 

104 

105 def get_stage_position(self) -> tbt.StagePositionUser: 

106 """Get current stage position. 

107 

108 Returns: 

109 Current stage position with x, y, z, r, t 

110 

111 Raises: 

112 MicroscopeConnectionError: If not connected or operation fails 

113 """ 

114 self.ensure_connected() 

115 

116 try: 

117 return factory.active_stage_position_settings(self._microscope) 

118 except Exception as e: 

119 raise MicroscopeConnectionError("Failed to get stage position") from e 

120 

121 def get_working_distances(self) -> Dict[str, float]: 

122 """Get current electron and ion beam working distances. 

123 

124 Returns: 

125 Dictionary with 'electron_wd_mm' and 'ion_wd_mm' keys 

126 

127 Raises: 

128 MicroscopeConnectionError: If not connected or operation fails 

129 """ 

130 self.ensure_connected() 

131 

132 try: 

133 electron_wd = ( 

134 ut.beam_type( 

135 tbt.ElectronBeam(tbt.BeamSettings()), self._microscope 

136 ).working_distance.value 

137 * Conversions.M_TO_MM 

138 ) 

139 

140 ion_wd = ( 

141 ut.beam_type( 

142 tbt.IonBeam(tbt.BeamSettings()), self._microscope 

143 ).working_distance.value 

144 * Conversions.M_TO_MM 

145 ) 

146 

147 return { 

148 "electron_wd_mm": electron_wd, 

149 "ion_wd_mm": ion_wd, 

150 } 

151 except Exception as e: 

152 raise MicroscopeConnectionError("Failed to get working distances") from e 

153 

154 def get_stage_info(self) -> Dict[str, any]: 

155 """Get comprehensive stage information. 

156 

157 Returns: 

158 Dictionary with position and working distance info 

159 

160 Raises: 

161 MicroscopeConnectionError: If not connected or operation fails 

162 """ 

163 self.ensure_connected() 

164 

165 try: 

166 position = self.get_stage_position() 

167 wds = self.get_working_distances() 

168 

169 return { 

170 "x_mm": position.x_mm, 

171 "y_mm": position.y_mm, 

172 "z_mm": position.z_mm, 

173 "r_deg": position.r_deg, 

174 "t_deg": position.t_deg, 

175 "electron_wd_mm": wds["electron_wd_mm"], 

176 "ion_wd_mm": wds["ion_wd_mm"], 

177 } 

178 except MicroscopeConnectionError: 

179 raise 

180 except Exception as e: 

181 raise MicroscopeConnectionError("Failed to get stage info") from e 

182 

183 def get_imaging_settings(self) -> tbt.ImageSettings: 

184 """Get current imaging settings. 

185 

186 Returns: 

187 Current image settings including beam, detector, scan parameters 

188 

189 Raises: 

190 MicroscopeConnectionError: If not connected or operation fails 

191 """ 

192 self.ensure_connected() 

193 

194 try: 

195 return factory.active_image_settings(self._microscope) 

196 except Exception as e: 

197 raise MicroscopeConnectionError("Failed to get imaging settings") from e 

198 

199 def get_laser_state(self) -> Dict: 

200 """Get current laser settings. 

201 

202 Returns: 

203 Dictionary of laser state parameters 

204 

205 Raises: 

206 MicroscopeConnectionError: If operation fails 

207 """ 

208 try: 

209 laser_state = factory.active_laser_state() 

210 return laser.laser_state_to_db(laser_state) 

211 except Exception as e: 

212 raise MicroscopeConnectionError("Failed to get laser state") from e 

213 

214 def test_connection(self) -> Tuple[bool, str]: 

215 """Test microscope connection. 

216 

217 Returns: 

218 Tuple of (success, message) 

219 """ 

220 try: 

221 self.connect() 

222 self.disconnect() 

223 return True, "Connection successful" 

224 except MicroscopeConnectionError as e: 

225 return False, str(e) 

226 except Exception as e: 

227 return False, f"Unexpected error: {e}" 

228 

229 

230def check_device_connections() -> Dict[str, bool]: 

231 """Check connections to peripheral devices (EDS, EBSD, laser). 

232 

233 Returns: 

234 Dictionary mapping device names to connection status 

235 

236 Note: 

237 This doesn't require a MicroscopeInterface instance as it 

238 checks device connections directly. 

239 """ 

240 try: 

241 status = laser._device_connections() 

242 return status 

243 except Exception as e: 

244 return {"error": str(e)} 

245 

246 

247def format_stage_info(stage_info: Dict[str, float]) -> str: 

248 """Format stage information for display. 

249 

250 Args: 

251 stage_info: Dictionary from get_stage_info() 

252 

253 Returns: 

254 Formatted multi-line string 

255 """ 

256 return ( 

257 f"EBeam WD = {stage_info['electron_wd_mm']:.5f} mm\n" 

258 f"IBeam WD = {stage_info['ion_wd_mm']:.5f} mm\n" 

259 f"\tX = {stage_info['x_mm']:.5f} mm\n" 

260 f"\tY = {stage_info['y_mm']:.5f} mm\n" 

261 f"\tZ = {stage_info['z_mm']:.5f} mm\n" 

262 f"\tR = {stage_info['r_deg']:.5f} °\n" 

263 f"\tT = {stage_info['t_deg']:.5f} °" 

264 )