-
Notifications
You must be signed in to change notification settings - Fork 13
/
coherent.py
381 lines (354 loc) · 17.2 KB
/
coherent.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
import serial
## NOTE: The OBIS laser box will autostart the lasers when power cycled!
## This means that the lasers will emit if the key is switched 'ON' and
## the interlock is 'CLOSED' (as is typically the case).
## The 'Autostart OFF' commands (commented out) below are attempts to fix
## this behaviour but so far they have not worked.
## The calls to '_set_auto_start_status(False, laser)' seem to work at
## the level of a single laser? But are then overwritten by the laser box
## during a power cycle? The manual does not say.
## Once the laser box is powered up (and python has control) the box and
## lasers behave as expected.
class OBIS:
def __init__(self,
which_port,
names_to_channels=None, # Dict: name to SCPI channel
operating_mode='CW-power', # String: 'CW-power' or 'AO-power'
verbose=True,
very_verbose=False):
self.verbose = verbose
self.very_verbose = very_verbose
if self.verbose: print('Initializing OBIS laser:')
try:
self.port = serial.Serial(port=which_port, timeout=1)
except serial.serialutil.SerialException:
raise IOError('No connection to OBIS on port %s'%which_port)
## self._send('SYSTem0:AUTostart OFF', reply=False) # Autostart OFF?
# Find devices
self.max_channels = 6
self.names_to_channels = {}
for ch in range(self.max_channels):
try:
self.get_device_identity(ch)
wavelength = self._send('SYSTem%i:INFormation:WAVelength?'%ch)
## self._send('Syst%i:Aut OFF'%ch, reply=False) # Autostart OFF?
self.names_to_channels[wavelength.split('.')[0]] = ch
except OSError as e:
if e.args[0] not in (
'Controller error: Device unavailable',
'Controller error: Unrecognized command/query'):
raise
# Use nicknames for channels (if passed on init)
if names_to_channels != None:
for name, channel in names_to_channels.items():
assert channel in self.device_identities
self.names_to_channels = names_to_channels
self.lasers = tuple(self.names_to_channels.keys())
self.warning_string = ('\nValid channel numbers: '+
str(list(self.names_to_channels)) +
'\nValid channel names: '+
str(list(self.names_to_channels.values())))
# Configure lasers
self.verbose = False # (set True for debugging)
self.power_setpoint_percent_min = {}
for laser in self.lasers:
self._set_CDRH_delay_status(False, laser) # Mandatory for enable
self._set_auto_start_status(False, laser) # Safety
self._get_device_type(laser) # Determines operating mode options
self.set_operating_mode(operating_mode, laser) # Also disables laser
pwr_min = self._get_power_min_watts(laser) # Required attribute
pwr_max = self._get_power_rating_watts(laser) # Required attribute
self.power_setpoint_percent_min[laser] = round(
(100 * pwr_min / pwr_max), 1) + 0.1 # + 0.1 to avoid round down
# add other attributes as needed
self.verbose = verbose
def n2c(self, name_or_channel):
if name_or_channel in range(self.max_channels):
return name_or_channel
elif name_or_channel in self.names_to_channels:
return self.names_to_channels[name_or_channel]
elif name_or_channel is None:
if len(self.names_to_channels) == 1:
return list(self.names_to_channels.values())[0]
else:
raise ValueError('Invalid name_or_channel: %s'%name_or_channel +
self.warning_string)
else:
raise ValueError('Invalid name_or_channel: %s'%name_or_channel +
self.warning_string)
def _send(self, cmd, reply=True):
assert isinstance(cmd, str)
cmd = bytes(cmd + '\r', 'ascii')
if self.very_verbose: print('Sending command:', cmd)
self.port.write(cmd)
response = None
if reply:
response = self.port.readline()
self._check_error(response)
handshake = self.port.readline()
if handshake != b'OK\r\n':
raise OSError('Unexpected handshake:', self._check_error(handshake))
assert self.port.in_waiting == 0
if self.very_verbose:
print('Response:', response)
print('Handshake:', handshake)
if not reply:
return None
return response.decode('ascii').strip('\r\n')
def _check_error(self, response):
if response[:3] == b'ERR':
ERR = response.decode('ascii').strip('\r\n')
error_codes = {
'ERR-400':'Query unavailable',
'ERR-350':'Queue overflow',
'ERR-321':'Out of memory',
'ERR-310':'System error',
'ERR-257':'File to open not named',
'ERR-256':'File does not exist',
'ERR-241':'Device unavailable',
'ERR-221':'Settings conflict',
'ERR-220':'Invalid parameter',
'ERR-203':'Command protected',
'ERR-200':'Execution error',
'ERR-109':'Parameter missing',
'ERR-102':'Syntax error',
'ERR-100':'Unrecognized command/query',
'ERR-000':'No error',
'ERR-500':'CCB fault',
'ERR-510':'I2C bus fault',
'ERR-520':'Controller time out',
'ERR-900':'CCB message timed out',
}
raise OSError('Controller error: ' + error_codes[ERR])
return None
def _get_auto_start_status(self, name=None):
channel = self.n2c(name)
auto_start_status = self._send('SYSTem%i:AUTostart?'%channel)
auto_start_status = {'ON': True, 'OFF': False}[auto_start_status]
if not hasattr(self, 'auto_start_status'):
self.auto_start_status = {}
self.auto_start_status[name] = auto_start_status
if self.verbose:
print('%s auto start status:'%name, auto_start_status)
return auto_start_status
def _set_auto_start_status(self, enable, name=None):
channel = self.n2c(name)
cmd = {True: 'ON', False: 'OFF'}[enable]
if self.very_verbose:
print('Setting %s auto start status to'%name, enable)
self._send('SYSTem%i:AUTostart '%channel + cmd, reply=False)
assert self._get_auto_start_status(name) == enable
return None
def _get_CDRH_delay_status(self, name=None):
channel = self.n2c(name)
CDRH_delay_status = self._send('SYSTem%i:CDRH?'%channel)
CDRH_delay_status = {'ON': True, 'OFF': False}[CDRH_delay_status]
if not hasattr(self, 'CDRH_delay_status'):
self.CDRH_delay_status = {}
self.CDRH_delay_status[name] = CDRH_delay_status
if self.verbose:
print('%s CDRH 5 second delay status:'%name,
CDRH_delay_status)
return CDRH_delay_status
def _set_CDRH_delay_status(self, enable, name=None):
channel = self.n2c(name)
cmd = {True: 'ON', False: 'OFF'}[enable]
if self.very_verbose:
print('Setting %s CDRH 5 second delay status to'%name, enable)
self._send('SYSTem%i:CDRH '%channel + cmd, reply=False)
assert self._get_CDRH_delay_status(name) == enable
return None
def _get_power_min_watts(self, name=None):
channel = self.n2c(name)
power_min_watts = float(self._send('SOURce%i:POWer:LIMit:LOW?'%channel))
if not hasattr(self, 'power_min_watts'):
self.power_min_watts = {}
self.power_min_watts[name] = power_min_watts
if self.verbose:
print('%s power minimum (watts):'%name, power_min_watts)
return power_min_watts
def _get_power_rating_watts(self, name=None):
channel = self.n2c(name)
power_rating_watts = float(
self._send('SYSTem%i:INFormation:POWer?'%channel))
if not hasattr(self, 'power_rating_watts'):
self.power_rating_watts = {}
self.power_rating_watts[name] = power_rating_watts
if self.verbose:
print('%s power rating (watts):'%name, power_rating_watts)
return power_rating_watts
def _get_device_type(self, name=None):
channel = self.n2c(name)
device_type = self._send('SYSTem%i:INFormation:TYPe?'%channel)
if not hasattr(self, 'device_types'):
self.device_types = {}
self.device_types[name] = device_type
if self.verbose:
print('%s device type:'%name, device_type)
return device_type
def get_device_identity(self, channel=None): # channel for unknown ID (init)
channel = self.n2c(channel)
device_identity = self._send('*IDN%i?'%channel)
if not hasattr(self, 'device_identities'):
self.device_identities = {}
self.device_identities[channel] = device_identity
if self.verbose:
print('Ch%s device identity:'%channel, device_identity)
return device_identity
def get_wavelength(self, name=None):
channel = self.n2c(name)
wavelength = self._send('SYSTem%i:INFormation:WAVelength?'%channel)
if not hasattr(self, 'wavelengths'):
self.wavelengths = {}
self.wavelengths[name] = wavelength
if self.verbose:
print('%s wavelength (nm):'%name, wavelength)
return wavelength
def get_operating_mode(self, name=None):
"""
Note: ***these modes depend on the exact OBIS model***
CWP = continuous wave, constant power
CWC = continuous wave, constant current
DIGITAL = CW with external digital modulation
ANALOG = CW with external analog modulation
MIXED = CW with external digital + analog modulation
DIGSO = External digital modulation with power feedback
MIXSO = External mixed modulation with power feedback
"""
channel = self.n2c(name)
operating_mode = self._send('SOURce%i:AM:SOURce?'%channel)
if operating_mode == 'CWP':
operating_mode = 'CW-power'
elif operating_mode in ('MIXED','MIXSO'):
operating_mode = 'AO-power'
else:
raise Exception('Unsupported operating mode %s'%operating_mode)
if not hasattr(self, 'operating_mode'):
self.operating_mode = {}
self.operating_mode[name] = operating_mode
if self.verbose:
print('%s operating mode:'%name, operating_mode)
return operating_mode
def set_operating_mode(self, mode, name=None):
channel = self.n2c(name)
self.set_enabled_status(False, name) # Required
if self.very_verbose: print('Setting %s mode to %s'%(name, mode))
if mode == 'CW-power': # power feedback with closed light-loop
self._send('SOURce%i:AM:INTernal CWP'%channel, reply=False)
elif mode == 'AO-power': # power feedback with closed light-loop
if self.device_types[name] == 'DDL':
self._send('SOURce%i:AM:EXTernal MIXSO'%channel, reply=False)
elif self.device_types[name] == 'OPSL':
self._send('SOURce%i:AM:EXTernal MIXed'%channel, reply=False)
else:
raise Exception('Unsupported operating mode %s'%mode)
assert self.get_operating_mode(name) == mode
return None
def get_power_setpoint_percent(self, name=None):
channel = self.n2c(name)
power_setpoint_watts = float(self._send(
'SOURce%i:POWer:LEVel:IMMediate:AMPLitude?'%channel))
if not hasattr(self, 'power_setpoint_watts'):
self.power_setpoint_watts = {}
self.power_setpoint_watts[name] = power_setpoint_watts
power_setpoint_percent = round( # max .dp
(100 * power_setpoint_watts / self.power_rating_watts[name]), 1)
if not hasattr(self, 'power_setpoint_percent'):
self.power_setpoint_percent = {}
self.power_setpoint_percent[name] = power_setpoint_percent
if self.verbose:
print('%s power setpoint: %0.1f%% (%f watts)'%(
name, power_setpoint_percent, power_setpoint_watts))
return power_setpoint_percent
def set_power_setpoint_percent(self, power_setpoint_percent, name=None):
channel = self.n2c(name)
if power_setpoint_percent == 'min':
power_setpoint_percent = self.power_setpoint_percent_min[name]
else:
assert isinstance(power_setpoint_percent, (int, float))
assert 0 <= power_setpoint_percent <= 100
power_setpoint_percent = round(power_setpoint_percent, 1) # max .dp
if power_setpoint_percent < (
self.power_setpoint_percent_min[name]):
raise Exception('Power setpoint percent %f < minimum (%f)'%(
power_setpoint_percent,
self.power_setpoint_percent_min[name]))
power_setpoint_watts = (
self.power_rating_watts[name] * power_setpoint_percent / 100)
if self.very_verbose:
print('Setting %s power setpoint to %0.1f%% (%f watts)'%(
name, power_setpoint_percent, power_setpoint_watts))
self._send('SOURce%i:POWer:LEVel:IMMediate:AMPLitude %f'%(
channel, power_setpoint_watts), reply=False)
assert self.get_power_setpoint_percent(name) == (
power_setpoint_percent)
return None
def get_enabled_status(self, name=None):
channel = self.n2c(name)
enabled_status = self._send('SOURce%i:AM:STATe?'%channel)
enabled_status = {'ON': True, 'OFF': False}[enabled_status]
if not hasattr(self, 'enabled_status'):
self.enabled_status = {}
self.enabled_status[name] = enabled_status
if self.verbose:
print('%s enabled status:'%name, enabled_status)
return enabled_status
### Turns laser ON!
def set_enabled_status(self, enable, name=None):
channel = self.n2c(name)
assert self.CDRH_delay_status[name] == False # No 5 second delay!
cmd = {True: 'ON', False: 'OFF'}[enable]
if self.very_verbose:
print('Setting %s enabled status to'%name, enable)
self._send(('SOURce%i:AM:STATe '%channel) + cmd, reply=False)
assert self.get_enabled_status(name) == enable
return None
def get_instantanous_power_watts(self, name=None, wait=True):
if wait:
from time import sleep
sleep(1) # Allow laser to settle before measurement
channel = self.n2c(name)
instantanous_power_watts = float(
self._send('SOURce%i:POWer:LEVel?'%channel))
if self.verbose:
print('%s instantanous power: %f watts'%(
name, instantanous_power_watts))
return instantanous_power_watts
def close(self):
for laser in self.lasers:
self.set_operating_mode('AO-power', laser) # Also disables laser
self.set_power_setpoint_percent('min', laser)
self._set_auto_start_status(False, laser)
if self.verbose: print('Closing OBIS COM port...', end='')
self.port.close()
if self.verbose: print('done.\n')
return None
if __name__ == '__main__':
n2c = {'UV-':1, 'Blu':2, 'Grn':3, 'Red':4} # optional nick names
laser_box = OBIS(which_port='COM4',
names_to_channels=n2c,
operating_mode='AO-power', # optional init to AO mode
verbose=True,
very_verbose=False)
# Call single lasers and access attributes:
laser_box.get_wavelength('Red')
laser_box.wavelengths['Red']
# Loop over all lasers and test all methods:
for laser in laser_box.lasers:
# Atypical methods for main block:
laser_box._set_auto_start_status(False, laser)
laser_box._set_CDRH_delay_status(False, laser) # setters call getters
laser_box._get_power_min_watts(laser)
laser_box._get_power_rating_watts(laser)
laser_box._get_device_type(laser)
laser_box.get_device_identity(laser)
laser_box.get_wavelength(laser)
# Typical methods/usage:
laser_box.set_operating_mode('CW-power', laser)
laser_box.set_power_setpoint_percent('min', laser)
laser_box.set_enabled_status(True, laser)
laser_box.set_power_setpoint_percent(5, laser)
laser_box.get_instantanous_power_watts(laser, wait=False)
laser_box.get_instantanous_power_watts(laser)
laser_box.set_enabled_status(False, laser)
laser_box.close()