|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
3 |
| -import atexit |
4 |
| -import datetime |
5 |
| -import json |
6 |
| -import pickle |
7 |
| -import socket |
8 |
| -import subprocess as sp |
9 |
| -import threading |
10 |
| -import time |
11 |
| -from functools import wraps |
12 |
| - |
13 |
| -from instamatic import config |
14 |
| -from instamatic.exceptions import TEMCommunicationError, exception_list |
15 |
| -from instamatic.server.serializer import dumper, loader |
16 |
| - |
17 |
| -HOST = config.settings.tem_server_host |
18 |
| -PORT = config.settings.tem_server_port |
19 |
| -BUFSIZE = 1024 |
20 |
| - |
21 |
| - |
22 |
| -class ServerError(Exception): |
23 |
| - pass |
24 |
| - |
25 |
| - |
26 |
| -def kill_server(p): |
27 |
| - # p.kill is not adequate |
28 |
| - sp.call(['taskkill', '/F', '/T', '/PID', str(p.pid)]) |
29 |
| - |
30 |
| - |
31 |
| -def start_server_in_subprocess(): |
32 |
| - cmd = 'instamatic.temserver.exe' |
33 |
| - p = sp.Popen(cmd, stdout=sp.DEVNULL) |
34 |
| - print(f'Starting TEM server ({HOST}:{PORT} on pid={p.pid})') |
35 |
| - atexit.register(kill_server, p) |
36 |
| - |
37 |
| - |
38 |
| -class MicroscopeClient: |
39 |
| - """Simulates a Microscope object and synchronizes calls over a socket |
40 |
| - server. |
41 |
| -
|
42 |
| - For documentation, see the actual python interface to the microscope |
43 |
| - API. |
44 |
| - """ |
45 |
| - |
46 |
| - def __init__(self, *, interface: str): |
47 |
| - super().__init__() |
48 |
| - |
49 |
| - self.interface = interface |
50 |
| - self.name = interface |
51 |
| - self._bufsize = BUFSIZE |
52 |
| - |
53 |
| - try: |
54 |
| - self.connect() |
55 |
| - except ConnectionRefusedError: |
56 |
| - start_server_in_subprocess() |
57 |
| - |
58 |
| - for t in range(30): |
59 |
| - try: |
60 |
| - self.connect() |
61 |
| - except ConnectionRefusedError: |
62 |
| - time.sleep(1) |
63 |
| - if t > 3: |
64 |
| - print('Waiting for server') |
65 |
| - if t > 30: |
66 |
| - raise TEMCommunicationError( |
67 |
| - 'Cannot establish server connection (timeout)' |
68 |
| - ) |
69 |
| - else: |
70 |
| - break |
71 |
| - |
72 |
| - self._init_dict() |
73 |
| - self.check_goniotool() |
74 |
| - |
75 |
| - atexit.register(self.s.close) |
76 |
| - |
77 |
| - def connect(self): |
78 |
| - self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
79 |
| - self.s.connect((HOST, PORT)) |
80 |
| - print(f'Connected to TEM server ({HOST}:{PORT})') |
81 |
| - |
82 |
| - def __getattr__(self, func_name): |
83 |
| - try: |
84 |
| - wrapped = self._dct[func_name] |
85 |
| - except KeyError as e: |
86 |
| - raise AttributeError( |
87 |
| - f'`{self.__class__.__name__}` object has no attribute `{func_name}`' |
88 |
| - ) from e |
89 |
| - |
90 |
| - @wraps(wrapped) |
91 |
| - def wrapper(*args, **kwargs): |
92 |
| - dct = {'func_name': func_name, 'args': args, 'kwargs': kwargs} |
93 |
| - return self._eval_dct(dct) |
94 |
| - |
95 |
| - return wrapper |
96 |
| - |
97 |
| - def _eval_dct(self, dct): |
98 |
| - """Takes approximately 0.2-0.3 ms per call if HOST=='localhost'.""" |
99 |
| - self.s.send(dumper(dct)) |
100 |
| - |
101 |
| - response = self.s.recv(self._bufsize) |
102 |
| - |
103 |
| - if response: |
104 |
| - status, data = loader(response) |
105 |
| - |
106 |
| - if status == 200: |
107 |
| - return data |
108 |
| - |
109 |
| - elif status == 500: |
110 |
| - error_code, args = data |
111 |
| - raise exception_list.get(error_code, TEMCommunicationError)(*args) |
112 |
| - |
113 |
| - else: |
114 |
| - raise ConnectionError(f'Unknown status code: {status}') |
115 |
| - |
116 |
| - def _init_dict(self): |
117 |
| - from instamatic.TEMController.microscope import get_tem |
118 |
| - |
119 |
| - tem = get_tem(interface=self.interface) |
120 |
| - |
121 |
| - self._dct = { |
122 |
| - key: value for key, value in tem.__dict__.items() if not key.startswith('_') |
123 |
| - } |
124 |
| - |
125 |
| - def __dir__(self): |
126 |
| - return self._dct.keys() |
127 |
| - |
128 |
| - def check_goniotool(self): |
129 |
| - """Check whether goniotool is available and update the config as |
130 |
| - necessary.""" |
131 |
| - if config.settings.use_goniotool: |
132 |
| - config.settings.use_goniotool = self.is_goniotool_available() |
133 |
| - |
134 |
| - |
135 |
| -class TraceVariable: |
136 |
| - """Simple class to trace a variable over time. |
137 |
| -
|
138 |
| - Usage: |
139 |
| - t = TraceVariable(ctrl.stage.get, verbose=True) |
140 |
| - t.start() |
141 |
| - t.stage.set(x=0, y=0, wait=False) |
142 |
| - ... |
143 |
| - values = t.stop() |
144 |
| - """ |
145 |
| - |
146 |
| - def __init__( |
147 |
| - self, |
148 |
| - func, |
149 |
| - interval: float = 1.0, |
150 |
| - name: str = 'variable', |
151 |
| - verbose: bool = False, |
152 |
| - ): |
153 |
| - super().__init__() |
154 |
| - self.name = name |
155 |
| - self.func = func |
156 |
| - self.interval = interval |
157 |
| - self.verbose = verbose |
158 |
| - |
159 |
| - self._traced = [] |
160 |
| - |
161 |
| - def start(self): |
162 |
| - print(f'Trace started: {self.name}') |
163 |
| - self.update() |
164 |
| - |
165 |
| - def stop(self): |
166 |
| - self._timer.cancel() |
167 |
| - |
168 |
| - print(f'Trace canceled: {self.name}') |
169 |
| - |
170 |
| - return self._traced |
171 |
| - |
172 |
| - def update(self): |
173 |
| - ret = self.func() |
174 |
| - |
175 |
| - now = datetime.datetime.now().strftime('%H:%M:%S.%f') |
176 |
| - |
177 |
| - if self.verbose: |
178 |
| - print(f'{now} | Trace {self.name}: {ret}') |
179 |
| - |
180 |
| - self._traced.append((now, ret)) |
181 |
| - |
182 |
| - self._timer = threading.Timer(self.interval, self.update) |
183 |
| - self._timer.start() |
| 3 | +from instamatic.microscope.client import MicroscopeClient |
0 commit comments