SEM_API.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. # -*- coding: utf-8 -*-
  2. """
  3. This is an implementation of SEM API Wrapper
  4. How to use:
  5. The SEM_API class provides a context manager. It is highly recommended to use
  6. it through _with_ expression
  7. with SEM_API(state='remote') as sem:
  8. do_something(sem)
  9. The SEM_API class must be initialized by specifying if the EM Server is running
  10. remotely or locally (state = 'remote' or 'local'). This will influence the
  11. behavior of how the real time image interface behaves.
  12. The interface provides following functions:
  13. 1. Functions derived from SmartSEM API:
  14. GetValue(AP_name, style='float')
  15. SetValue(AP_name, value)
  16. GetValueMin(AP_name)
  17. GetValueMax(AP_name)
  18. GetState(DP_name, style='string')
  19. SetState(DP_name, value)
  20. Execute(CMD_name)
  21. GetStagePosition()
  22. MoveStage(coord)
  23. Grab(self, fname, left = 0.0, right = 1.0, top = 0.0, bottom = 1.0, overlay = False)
  24. 2. Interface to get real time image. The real time image will be updated in
  25. following properties:
  26. img_array
  27. If running remotely, it provides uint8 array of current image using the current
  28. pixel store. If running locally, it provides uint16 array instead.
  29. img_array_reduced
  30. Only present if running locally. It provides uint8 array of current image in
  31. 1024x768 pixel store.
  32. The following commands control the start, pause and terminate of continuous
  33. image acquisition:
  34. UpdateImage_Start()
  35. UpdateImage_Pause()
  36. UpdateImage_Terminate()
  37. The update rate can be set through the following property:
  38. update_rate
  39. possible values from 0.01 to 10
  40. 3. Often used high level function:
  41. wait_for_stage_idle()
  42. Wait till the stage arrives in destination.
  43. grab_full_image(fname)
  44. Freeze and wait till scan finished to capture a full image.
  45. set_scan_speed(scan_speed)
  46. Directly set certain scan speed.
  47. set_scan_mode(scan_mode)
  48. Directly set the noise reduction mode of scan.
  49. do_autofocus()
  50. Peform autofocus, block the thread till it finishes.
  51. 4. Event interface:
  52. The event interface is implemented as "bag of handler function". The handler function is
  53. called as:
  54. handler(ParameterName, Reason, ParameterID, LastKnownValue)
  55. The following function and attributes are used to control the event interface:
  56. Add_Event(func)
  57. Add func in the bag of handler.
  58. Remove_Event(func)
  59. Remove func from the bag of handler.
  60. Set_Notify(PARAM)
  61. Start monitoring PARAM, trigger the event when PARAM is changed.
  62. Unset_Notify(PARAM)
  63. Stop monitoring PARAM.
  64. event_time
  65. Set the time interval in second to check event trigger. Default is 0.1 S.
  66. Version: 0.3
  67. Author: Luyang Han
  68. ChangLog:
  69. v0.3
  70. 04.09.2019 : Add event interface
  71. """
  72. import numpy as np
  73. import sys
  74. sys.coinit_flags = 0
  75. from win32com import client
  76. import pythoncom
  77. import threading, time, tempfile, mmap, warnings
  78. try:
  79. from imageio import imread
  80. except:
  81. try:
  82. from scipy.misc import imread
  83. except:
  84. try:
  85. from skimage.io import imread
  86. except:
  87. print("Cannot find the module with imread. Please install one of the following package:\n imageio, skimage or scipy.")
  88. # following changes are necessary for event to work
  89. from win32com.client import makepy
  90. sys.argv = ["makepy", r"CZ.EmApiCtrl.1"]
  91. makepy.main()
  92. _excpetion_dict = \
  93. {1000: ('API_E_GET_TRANSLATE_FAIL',
  94. 'Failed to translate parameter into an id'),
  95. 1001: ('API_E_GET_AP_FAIL', 'Failed to get analogue value'),
  96. 1002: ('API_E_GET_DP_FAIL', 'Failed to get digital value'),
  97. 1003: ('API_E_GET_BAD_PARAMETER',
  98. 'Parameter supplied is not analogue nor digital'),
  99. 1004: ('API_E_SET_TRANSLATE_FAIL',
  100. 'Failed to translate parameter into an id'),
  101. 1005: ('API_E_SET_STATE_FAIL', 'Failed to set a digital state'),
  102. 1006: ('API_E_SET_FLOAT_FAIL', 'Failed to set a float value'),
  103. 1007: ('API_E_SET_FLOAT_LIMIT_LOW', 'Value supplied is too low'),
  104. 1008: ('API_E_SET_FLOAT_LIMIT_HIGH', 'Value supplied is too high'),
  105. 1009: ('API_E_SET_BAD_VALUE', 'Value supplied is is of wrong type'),
  106. 1010: ('API_E_SET_BAD_PARAMETER',
  107. 'Parameter supplied is not analogue nor digital'),
  108. 1011: ('API_E_EXEC_TRANSLATE_FAIL', 'Failed to translate command into an id'),
  109. 1012: ('API_E_EXEC_CMD_FAIL', 'Failed to execute command'),
  110. 1013: ('API_E_EXEC_MCF_FAIL', 'Failed to execute file macro'),
  111. 1014: ('API_E_EXEC_MCL_FAIL', 'Failed to execute library macro'),
  112. 1015: ('API_E_EXEC_BAD_COMMAND', 'Command supplied is not implemented'),
  113. 1016: ('API_E_GRAB_FAIL', 'Grab command failed'),
  114. 1017: ('API_E_GET_STAGE_FAIL', 'Get Stage position failed'),
  115. 1018: ('API_E_MOVE_STAGE_FAIL', 'Move Stage position failed'),
  116. 1019: ('API_E_NOT_INITIALISED', 'API not initialised'),
  117. 1020: ('API_E_NOTIFY_TRANSLATE_FAIL',
  118. 'Failed to translate parameter to an id'),
  119. 1021: ('API_E_NOTIFY_SET_FAIL', 'Set notification failed'),
  120. 1022: ('API_E_GET_LIMITS_FAIL', 'Get limits failed'),
  121. 1023: ('API_E_GET_MULTI_FAIL', 'Get multiple parameters failed'),
  122. 1024: ('API_E_SET_MULTI_FAIL', 'Set multiple parameters failed'),
  123. 1025: ('API_E_NOT_LICENSED', 'Missing API license'),
  124. 1026: ('API_E_NOT_IMPLEMENTED', 'Reserved or not implemented'),
  125. 1027: ('API_E_GET_USER_NAME_FAIL',
  126. 'Failed to get user name (Remoting Interface only)'),
  127. 1028: ('API_E_GET_USER_IDLE_FAIL',
  128. 'Failed to get user idle state (Remoting Interface only)'),
  129. 1029: ('API_E_GET_LAST_REMOTING_CONNECT_ERROR_FAIL',
  130. 'Failed to get the last remoting connection error string (Remoting Interface Only)'),
  131. 1030: ('API_E_EMSERVER_LOGON_FAILED',
  132. 'Failed to remotely logon to the EM Server (username and password may be incorrect or EM Server is not running or User is already logged on, Remoting only)'),
  133. 1031: ('API_E_EMSERVER_START_FAILED',
  134. 'Failed to start the EM Server. This may be because the Server is already running or has an internal error (Remoting Interface only)'),
  135. 1032: ('API_E_PARAMETER_IS_DISABLED',
  136. 'The command or parameter is currently disabled (you cannot execute or set it. Remoting Interface only)'),
  137. 2027: ('API_E_REMOTING_NOT_CONFIGURED',
  138. 'Remoting incorrectly configured, use RConfigure to correct'),
  139. 2028: ('API_E_REMOTING_FAILED_TO_CONNECT',
  140. 'Remoting did not connect to the server'),
  141. 2029: ('API_E_REMOTING_COULD_NOT_CREATE_INTERFACE',
  142. 'Remoting could not start (unknown reason)'),
  143. 2030: ('API_E_REMOTING_EMSERVER_NOT_RUNNING',
  144. 'Remoting: Remote server is not running currently'),
  145. 2031: ('API_E_REMOTING_NO_USER_LOGGED_IN',
  146. 'Remoting: Remote server has no user logged in')}
  147. # class to handle exceptions
  148. class API_ERROR(Exception):
  149. def __init__(self, error_code):
  150. global _excpetion_dict
  151. error_text = ('\n').join(_excpetion_dict[error_code])
  152. raise Exception(error_text)
  153. # periodically run function
  154. class SwitchThread(threading.Thread):
  155. """
  156. class to run a function repeatedly in the background.
  157. The delay between each run is given as delay in s.
  158. The thread can be paused and also ended.
  159. """
  160. def __init__(self, target=None, name=None, args=(), kwargs={}, delay = 1):
  161. super(SwitchThread, self).__init__(group=None, target=target, name=name, args=(), kwargs={})
  162. self.target = target
  163. self.args = args
  164. self.kwargs = kwargs
  165. self.delay = delay
  166. self.enable = True
  167. self.stop = False
  168. return
  169. def run(self):
  170. while not self.stop:
  171. if self.enable:
  172. self.target(*self.args, **self.kwargs)
  173. time.sleep(self.delay)
  174. def pause(self):
  175. self.enable = False
  176. def resume(self):
  177. self.enable = True
  178. def terminate(self):
  179. self.stop = True
  180. EventBase = client.getevents("CZ.EMApiCtrl.1")
  181. class SEM_Handle(EventBase):
  182. def __init__(self, sem):
  183. self.subscribers = []
  184. super().__init__(sem)
  185. def OnNotifyWithCurrentValue(self, lpszParameter, Reason, paramid, dLastKnownValue):
  186. for sub in self.subscribers:
  187. sub (lpszParameter, Reason, paramid, dLastKnownValue)
  188. # EventBase = client.getevents("CZ.EMApiCtrl.1")
  189. # class SEM_Handle(EventBase):
  190. # def OnNotifyWithCurrentValue(self, lpszParameter, Reason, paramid, dLastKnownValue):
  191. # print("Reason:%d. The parameter %s changed to %d." %(Reason, lpszParameter, dLastKnownValue))
  192. class SEM_API:
  193. """
  194. Python Wrapper to SEM API interface
  195. The class provide a context manager to do some house keeping, it is advised
  196. to use with statement.
  197. with SEM_API(state='remote') as sem:
  198. do_something(sem)
  199. """
  200. def __init__(self, state='local'):
  201. """
  202. Function to initialize the API interface.
  203. This function uses the long InitialiseRemote(void) command.
  204. """
  205. self.mic = client.Dispatch('CZ.EMApiCtrl.1')
  206. if not (state == "remote" or state == "local"):
  207. raise ValueError("state is either remote or local")
  208. self.__state = state
  209. # first initialize
  210. res = self.mic.InitialiseRemoting()
  211. if res == 0:
  212. # prepare the background working
  213. # prepare the MMF for local operation
  214. if self.__state == "local":
  215. self.__pymap = mmap.mmap(-1, 1024*768+1064,"Capture0.MMF",mmap.ACCESS_READ)
  216. time.sleep(0.1)
  217. self.__background_worker = SwitchThread(target=self.__update_image, delay = 1.0)
  218. self.__background_worker.start()
  219. self.__background_worker.pause()
  220. time.sleep(0.1)
  221. # self.subs = []
  222. # self.event = SEM_Handle(self.mic, self.subs)
  223. self.event_time = 0.1
  224. self.event = SEM_Handle(self.mic)
  225. self.__event_stop = False
  226. self.__event_thread = threading.Thread(target = self.__pump)
  227. self.__event_thread.start()
  228. else:
  229. raise API_ERROR(res)
  230. # decorator for the wrapper function
  231. def __error_handling(func):
  232. def func_wrapper(*arg, **kwargs):
  233. res = func(*arg, **kwargs)
  234. if type(res) == int:
  235. return_code = res
  236. if return_code != 0:
  237. raise API_ERROR(return_code)
  238. elif type(res) == tuple:
  239. return_code = res[0]
  240. result = res[1]
  241. if return_code == 0:
  242. return result
  243. else:
  244. raise API_ERROR(return_code)
  245. return func_wrapper
  246. # function to handle the event
  247. def __pump(self):
  248. pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)
  249. while self.__event_stop is not True:
  250. pythoncom.PumpWaitingMessages()
  251. time.sleep(self.event_time)
  252. pythoncom.CoUninitialize()
  253. def Add_Event(self, func):
  254. self.event.subscribers.append(func)
  255. def Remove_Event(self, func):
  256. self.event.subscribers.remove(func)
  257. @__error_handling
  258. def Set_Notify(self, PARAM):
  259. res = self.mic.SetNotify(PARAM, True)
  260. return res
  261. @__error_handling
  262. def Unset_Notify(self, PARAM):
  263. res = self.mic.SetNotify(PARAM, False)
  264. return res
  265. # some variables to hold the data
  266. __fname = tempfile.TemporaryFile(suffix='.bmp').name
  267. # function to update the image file
  268. def __update_image(self):
  269. if self.__state == 'remote':
  270. # when working remote, write the image to a bmp file and read back.
  271. self.mic.Grab(0,0,1024,768,0,self.__fname)
  272. self.img_array = imread(self.__fname)
  273. elif self.__state == 'local':
  274. # when working local, write the image to mmap file and read back.
  275. # as each time the size of the frame can change, we create a mmap
  276. # every time on the fly to grab the image
  277. pixel_density = tuple(map(int,self.GetState("DP_IMAGE_STORE").split("*")))
  278. self.mic.Grab(0,0,1024,768,0,"CZ.MMF")
  279. py_map = mmap.mmap(-1, pixel_density[0]*pixel_density[1]*2,"CZ.MMF",mmap.ACCESS_READ)
  280. self.img_array = np.copy(np.frombuffer(py_map, dtype="uint16").reshape((pixel_density[1],pixel_density[0])))
  281. py_map.close()
  282. # update local image
  283. self.img_array_reduce = np.flip(np.frombuffer(self.__pymap, dtype="uint8", offset=1064).reshape((768,1024)), axis=0)
  284. return
  285. @property
  286. def update_rate(self):
  287. return self.__background_worker.delay
  288. @update_rate.setter
  289. def update_rate(self, value):
  290. if value < 0.01:
  291. value = 0.01
  292. warnings.warn("Update delay cannot be smaller than 10 ms.")
  293. elif value > 10:
  294. value = 10
  295. warnings.warn("Update delay cannot be larger than 10 s.")
  296. self.__background_worker.delay = value
  297. # function for AP
  298. @__error_handling
  299. def GetValue(self, AP_name, style='float'):
  300. """
  301. function to get an analog value.
  302. style: float or string
  303. float returns a floating number, string returns a string representation with unit.
  304. """
  305. if style == 'float':
  306. buffer = 0.0
  307. res = self.mic.Get(AP_name, buffer)
  308. return res
  309. elif style == 'string':
  310. buffer = ''
  311. res = self.mic.Get(AP_name, buffer)
  312. return res
  313. else:
  314. raise AttributeError("style = float or string")
  315. @__error_handling
  316. def GetValueMin(self, AP_name):
  317. buffer = 0.0
  318. res = self.mic.GetLimits(AP_name, buffer, buffer)
  319. return (res[0], res[1])
  320. @__error_handling
  321. def GetValueMax(self, AP_name):
  322. buffer = 0.0
  323. res = self.mic.GetLimits(AP_name, buffer, buffer)
  324. return (res[0], res[2])
  325. @__error_handling
  326. def SetValue(self, AP_name, value):
  327. """
  328. function to set an analog value.
  329. The function can take either floating point or a formatted string.
  330. """
  331. if type(value) == str:
  332. pass
  333. else:
  334. value = float(value)
  335. res = self.mic.Set(AP_name, value)
  336. return res
  337. # function for DP
  338. @__error_handling
  339. def GetState(self, DP_name, style='string'):
  340. """
  341. function to get an digital value.
  342. style: float or string
  343. int returns a integer number, string returns a string representation.
  344. """
  345. if style == 'int':
  346. buffer = 0
  347. res = self.mic.Get(DP_name, buffer)
  348. return res
  349. elif style == 'string':
  350. buffer = ''
  351. res = self.mic.Get(DP_name, buffer)
  352. return res
  353. else:
  354. raise AttributeError("style = int or string")
  355. @__error_handling
  356. def SetState(self, DP_name, value):
  357. """
  358. function to set a digital state.
  359. The function can take either integer or a formatted string.
  360. """
  361. if type(value) == str:
  362. pass
  363. else:
  364. value = int(value)
  365. res = self.mic.Set(DP_name, value)
  366. return res
  367. # function for CMD
  368. @__error_handling
  369. def Execute(self, CMD_name):
  370. res = self.mic.Execute(CMD_name)
  371. return res
  372. # function about stage
  373. @__error_handling
  374. def GetStagePosition(self):
  375. buffer = 0.0
  376. res = self.mic.GetStagePosition(buffer, buffer, buffer, buffer, buffer, buffer)
  377. return (res[0], res[1:])
  378. @__error_handling
  379. def MoveStage(self, coord):
  380. if len(coord) == 6:
  381. res = self.mic.MoveStage(*coord)
  382. return res
  383. else:
  384. raise IndexError("coord must be a list or tuple of 6 floating number")
  385. # function about grab image
  386. @__error_handling
  387. def Grab(self, fname, X = 0, Y = 0, W = 1024, H = 768, overlay = False):
  388. """
  389. The function to grab the current image.
  390. left, right, top, button defines the desired size of the imaged, express as the
  391. coordinate relative to the full image.
  392. For example, if 10% of the edge should be excluded, the coordinate should be
  393. left = 0.1, right = 0.9, top = 0,1, button = 0.9
  394. overlay determines whether the image overlay such as datazone should be included.
  395. """
  396. #X = int(left * 1024)
  397. #Y = int(top * 768)
  398. #W = int((right-left)*1024)
  399. #H = int((bottom-top)*768)
  400. if overlay:
  401. res = self.mic.Grab(X,Y,W,H,-1,fname)
  402. else:
  403. res = self.mic.Grab(X,Y,W,H,0,fname)
  404. return res
  405. # parameter and functions dealing with the memory map file and real time imaging.
  406. # implementation pending
  407. # The real time image access only works with local client.
  408. # 2 different interface are supplied, the img_array gives the real time image with
  409. # current pixel density in 16 bit grey. the image_array_reduced gives 8 bit grey
  410. # image of image reduced to 1024x768
  411. # the value should be updated regularly by a seperate thread in the background.
  412. img_array = np.zeros((768,1024))
  413. img_array_reduce = np.zeros((768,1024))
  414. # img_array = np.zeros((768,1024)).astype('uint16')
  415. def UpdateImage_Start(self):
  416. """
  417. function to start continuous updating the imaging array
  418. """
  419. self.__background_worker.resume()
  420. def UpdateImage_Pause(self):
  421. """
  422. function to pause continuous updating the imaging array
  423. """
  424. self.__background_worker.pause()
  425. # some typically used higher level functions
  426. # implementation pending
  427. def move_stage_relative_xy(self, dx, dy):
  428. pos = self.GetStagePosition()
  429. self.MoveStage((pos[0] + dx, pos[1]+dy, pos[2],pos[3],pos[4],pos[5]))
  430. def wait_for_stage_idle(self):
  431. """
  432. wait for the stage movement to finish.
  433. """
  434. while not self.GetState("DP_STAGE_IS") == "Idle":
  435. time.sleep(0.1)
  436. def set_scan_speed(self, DP_Param):
  437. c_dict = {
  438. "0":"CMD_SCANRATE0",
  439. "1":"CMD_SCANRATE1",
  440. "2":"CMD_SCANRATE2",
  441. "3":"CMD_SCANRATE3",
  442. "4":"CMD_SCANRATE4",
  443. "5":"CMD_SCANRATE5",
  444. "6":"CMD_SCANRATE6",
  445. "7":"CMD_SCANRATE7",
  446. "8":"CMD_SCANRATE8",
  447. "9":"CMD_SCANRATE9",
  448. "10":"CMD_SCANRATE10",
  449. "11":"CMD_SCANRATE11",
  450. "12":"CMD_SCANRATE12",
  451. "13":"CMD_SCANRATE13",
  452. "14":"CMD_SCANRATE14",
  453. "15":"CMD_SCANRATE15"
  454. }
  455. self.Execute(c_dict[str(DP_Param)])
  456. def set_scan_mode(self, DP_Param):
  457. c_dict = {
  458. "Pixel Avg.":"CMD_PIXNR",
  459. "Frame Avg":"CMD_FRAME_AVERAGE",
  460. "Frame Int. Busy":"CMD_FRAME_INT",
  461. "Frame Int. Done":"CMD_FRAME_INT",
  462. "Line Avg":"CMD_LINE_AVG",
  463. "Line Int. Busy":"CMD_LINE_INT",
  464. "Line Int. Done":"CMD_LINE_INT",
  465. "Continuous Avg.":"CMD_CONTINUOUS_AVG",
  466. "Drift Comp. Frame Int. Busy":"CMD_DC_FRAME_INT",
  467. "Drift Comp. Frame Int. Done":"CMD_DC_FRAME_INT",
  468. "Drift Comp. Frame Avg.":"CMD_DC_FRAME_AVG"
  469. }
  470. self.Execute(c_dict[DP_Param])
  471. def do_autofocus(self):
  472. self.SetState("DP_AUTOFOCUS_VERSION", "Line Scan based Autofocus Version")
  473. time.sleep(0.5)
  474. self.Execute("CMD_AUTO_FOCUS_FINE")
  475. time.sleep(2)
  476. while self.GetState("DP_AUTO_FN_STATUS") == "Busy":
  477. time.sleep(0.1)
  478. def grab_full_image(self, fname, overlay = False):
  479. """
  480. grab the current full image by restarting the scan and till the complete
  481. frame is finished, and then save the image.
  482. """
  483. self.SetState("DP_FREEZE_ON", "End Frame")
  484. self.Execute("CMD_UNFREEZE_ALL")
  485. time.sleep(0.1)
  486. #self.Execute("CMD_MODE_NORMAL")
  487. #time.sleep(0.1)
  488. if self.GetState("DP_NOISE_REDUCTION") in ("Frame Avg","Line Avg",\
  489. "Pixel Avg.", "Continuous Avg.", "Drift Comp. Frame Avg."):
  490. time.sleep(0.1)
  491. self.Execute("CMD_FREEZE_ALL")
  492. while self.GetState("DP_FROZEN") == "Live":
  493. time.sleep(0.1)
  494. self.Grab(fname, overlay = overlay)
  495. # context interface
  496. # it is important to use this class through with statement
  497. # the exit function will properly clean up the wrapper.
  498. def __enter__(self):
  499. return self
  500. def __exit__(self, *arg):
  501. self.__background_worker.terminate()
  502. self.__background_worker.join()
  503. # stop event interface
  504. self.__event_stop = True
  505. self.__event_thread.join()
  506. self.event.close()
  507. del self.event
  508. # clear the MMF
  509. if self.__state == 'local':
  510. self.__pymap.close()
  511. self.mic.Grab(0,0,0,0,0,"CZ.MMF")
  512. self.mic.ClosingControl()