epicsdev_ps_caen_fastps 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epicsdev_ps_caen_fastps/__init__.py +0 -0
- epicsdev_ps_caen_fastps/__main__.py +375 -0
- epicsdev_ps_caen_fastps-0.0.3.dist-info/METADATA +113 -0
- epicsdev_ps_caen_fastps-0.0.3.dist-info/RECORD +6 -0
- epicsdev_ps_caen_fastps-0.0.3.dist-info/WHEEL +4 -0
- epicsdev_ps_caen_fastps-0.0.3.dist-info/licenses/LICENSE +28 -0
|
File without changes
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""EPICS PVAccess server for CAEN FAST-PS power supply."""
|
|
2
|
+
# pylint: disable=invalid-name,broad-exception-caught
|
|
3
|
+
__version__ = 'v0.0.3 2026-09-06'
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import re
|
|
7
|
+
import socket
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
from epicsdev import epicsdev as edev
|
|
12
|
+
|
|
13
|
+
DEFAULT_HOST = '130.199.104.57'
|
|
14
|
+
DEFAULT_PORT = 10001
|
|
15
|
+
DEFAULT_TIMEOUT = 2.0
|
|
16
|
+
pargs = None
|
|
17
|
+
SetpointLimits = {#model: [minV, maxV, minI, maxI]
|
|
18
|
+
'FAST-PS 0520-100': [-20.0, 20.0, -5.0, 5.0],
|
|
19
|
+
'FAST-PS 0540-200': [-40.0, 40.0, -5.0, 5.0],
|
|
20
|
+
'FAST-PS 1020-200': [-20.0, 20.0, -10.0, 10.0],
|
|
21
|
+
'FAST-PS 0580-400': [-80.0, 80.0, -5.0, 5.0],
|
|
22
|
+
'FAST-PS 1040-400': [-40.0, 40.0, -10.0, 10.0],
|
|
23
|
+
'FAST-PS 2020-400': [-20.0, 20.0, -20.0, 20.0],
|
|
24
|
+
'FAST-PS 2040-600': [-40.0, 40.0, -20.0, 20.0],
|
|
25
|
+
'FAST-PS 3020-600': [-20.0, 20.0,-30.0, 30.0],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class C_:
|
|
29
|
+
"""Namespace for module state."""
|
|
30
|
+
model = "/"
|
|
31
|
+
sock = None
|
|
32
|
+
PvDefs = []
|
|
33
|
+
|
|
34
|
+
def handle_exception(where: str):
|
|
35
|
+
"""Log exceptions through status/error PVs."""
|
|
36
|
+
edev.printe(f'{where}: {sys.exc_info()[1]}')
|
|
37
|
+
|
|
38
|
+
def _connect():
|
|
39
|
+
"""Open TCP socket to FAST-PS."""
|
|
40
|
+
try:
|
|
41
|
+
C_.sock = socket.create_connection((pargs.host, pargs.port), timeout=pargs.timeout)
|
|
42
|
+
C_.sock.settimeout(pargs.timeout)
|
|
43
|
+
edev.printi(f'Connected to FAST-PS at {pargs.host}:{pargs.port}')
|
|
44
|
+
except OSError:
|
|
45
|
+
handle_exception(f'connecting to {pargs.host}:{pargs.port}')
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
def _read_line() -> str:
|
|
49
|
+
"""Read one reply line from the socket."""
|
|
50
|
+
if C_.sock is None:
|
|
51
|
+
raise RuntimeError('Socket is not connected')
|
|
52
|
+
|
|
53
|
+
data = bytearray()
|
|
54
|
+
while True:
|
|
55
|
+
chunk = C_.sock.recv(1)
|
|
56
|
+
if not chunk:
|
|
57
|
+
break
|
|
58
|
+
data.extend(chunk)
|
|
59
|
+
if chunk == b'\n':
|
|
60
|
+
break
|
|
61
|
+
return data.decode('ascii', errors='ignore').strip()
|
|
62
|
+
|
|
63
|
+
def _send(cmd: str, updateStatus = False) -> str:
|
|
64
|
+
"""Send ASCII command terminated with CR and return one-line reply."""
|
|
65
|
+
if C_.sock is None:
|
|
66
|
+
raise RuntimeError('Socket is not connected')
|
|
67
|
+
|
|
68
|
+
wire = f'{cmd}\r'.encode('ascii', errors='ignore')
|
|
69
|
+
edev.printv(f'Sending command: {cmd!r} -> {wire!r}, updateStatus={updateStatus}')
|
|
70
|
+
C_.sock.sendall(wire)
|
|
71
|
+
reply = _read_line()
|
|
72
|
+
if not reply:
|
|
73
|
+
raise RuntimeError(f'Empty reply for command {cmd!r}')
|
|
74
|
+
if updateStatus:
|
|
75
|
+
edev.publish('status', '')# clear status before sending command
|
|
76
|
+
_query_status()
|
|
77
|
+
return reply
|
|
78
|
+
|
|
79
|
+
def _is_ack(reply: str) -> bool:
|
|
80
|
+
r = reply.upper()
|
|
81
|
+
return '#AK' in r or r == 'AK'
|
|
82
|
+
|
|
83
|
+
def _parse_first_float(text: str):
|
|
84
|
+
m = re.search(r'[-+]?\d+(?:\.\d*)?(?:[eE][-+]?\d+)?', str(text))
|
|
85
|
+
return float(m.group(0)) if m else None
|
|
86
|
+
|
|
87
|
+
def _parse_status32(reply: str) -> int:
|
|
88
|
+
"""Parse MST reply value (hex preferred, int fallback)."""
|
|
89
|
+
s = reply.strip()
|
|
90
|
+
if ':' in s:
|
|
91
|
+
s = s.split(':')[-1].strip()
|
|
92
|
+
s = s.replace('#', '').replace('0x', '').replace('0X', '').strip()
|
|
93
|
+
|
|
94
|
+
if re.fullmatch(r'[0-9A-Fa-f]+', s):
|
|
95
|
+
return int(s, 16)
|
|
96
|
+
|
|
97
|
+
m = re.search(r'\d+', s)
|
|
98
|
+
if m:
|
|
99
|
+
return int(m.group(0))
|
|
100
|
+
raise ValueError(f'Cannot parse status from reply {reply!r}')
|
|
101
|
+
|
|
102
|
+
def _query_float(cmd: str, default=None):
|
|
103
|
+
try:
|
|
104
|
+
reply = _send(cmd)
|
|
105
|
+
value = _parse_first_float(reply)
|
|
106
|
+
return default if value is None else value
|
|
107
|
+
except Exception:
|
|
108
|
+
handle_exception(f'in _query_float({cmd})')
|
|
109
|
+
return default
|
|
110
|
+
|
|
111
|
+
def _query_text(cmd: str, default='') -> str:
|
|
112
|
+
try:
|
|
113
|
+
return _send(cmd)
|
|
114
|
+
except Exception:
|
|
115
|
+
handle_exception(f'in _query_text({cmd})')
|
|
116
|
+
return default
|
|
117
|
+
|
|
118
|
+
def _query_status():
|
|
119
|
+
try:
|
|
120
|
+
reply = _send('MST')
|
|
121
|
+
status32 = _parse_status32(reply)
|
|
122
|
+
status_lsb = status32 & 0xFFFF
|
|
123
|
+
status_msb = (status32 >> 16) & 0xFFFF
|
|
124
|
+
edev.publish('StatusLSB', status_lsb, ifChanged=True)
|
|
125
|
+
edev.publish('StatusMSB', status_msb, ifChanged=True)
|
|
126
|
+
#edev.publish('_EnableInit', status_lsb, ifChanged=True)
|
|
127
|
+
enable = 1 if (status_lsb & 0x1) else 0
|
|
128
|
+
#edev.publish('_EnableInitCalc', enable, ifChanged=True)
|
|
129
|
+
edev.publish('Enable', enable, ifChanged=True)
|
|
130
|
+
reply = _send('UPMODE:?')
|
|
131
|
+
mode = reply.split(':')[-1].strip()
|
|
132
|
+
edev.publish('Upmode', mode, ifChanged=True)
|
|
133
|
+
reply = _send('MSRV:?')
|
|
134
|
+
rate_v = _parse_first_float(reply)
|
|
135
|
+
edev.publish('RampRateV', rate_v, ifChanged=True)
|
|
136
|
+
reply = _send('MSRI:?')
|
|
137
|
+
rate_i = _parse_first_float(reply)
|
|
138
|
+
edev.publish('RampRateI', rate_i, ifChanged=True)
|
|
139
|
+
|
|
140
|
+
except Exception:
|
|
141
|
+
handle_exception('in _query_status')
|
|
142
|
+
|
|
143
|
+
def set_regulation_mode(value, *_):
|
|
144
|
+
try:
|
|
145
|
+
mode = str(value).strip().upper()
|
|
146
|
+
mode = 'I' if mode.startswith('I') or mode == '1' else 'V'
|
|
147
|
+
reply = _send(f'LOOP {mode}', updateStatus=True)
|
|
148
|
+
if not _is_ack(reply):
|
|
149
|
+
raise RuntimeError(f'Unexpected reply for LOOP: {reply}')
|
|
150
|
+
edev.publish('RegulationMode', mode, ifChanged=True)
|
|
151
|
+
except Exception:
|
|
152
|
+
handle_exception('in set_regulation_mode')
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _set_setpoint(value: float, kind: str):
|
|
156
|
+
use_ramp = str(edev.pvv('RampEnable')).upper() in ('1', 'ON', 'TRUE')
|
|
157
|
+
cmd = f'MW{kind}R:{value}' if use_ramp else f'MW{kind}:{value}'
|
|
158
|
+
print(f'Setting {kind} setpoint to {value} (ramp: {use_ramp}, cmd: {cmd})')
|
|
159
|
+
reply = _send(cmd, updateStatus=True)
|
|
160
|
+
if not _is_ack(reply):
|
|
161
|
+
raise RuntimeError(f'Unexpected reply for {cmd}: {reply}')
|
|
162
|
+
|
|
163
|
+
def set_voltage(value, *_):
|
|
164
|
+
try:
|
|
165
|
+
voltage = float(value)
|
|
166
|
+
_set_setpoint(voltage, 'V')
|
|
167
|
+
edev.publish('Voltage', voltage, ifChanged=True)
|
|
168
|
+
except Exception:
|
|
169
|
+
handle_exception('in set_voltage')
|
|
170
|
+
|
|
171
|
+
def set_current(value, *_):
|
|
172
|
+
try:
|
|
173
|
+
current = float(value)
|
|
174
|
+
_set_setpoint(current, 'I')
|
|
175
|
+
edev.publish('Current', current, ifChanged=True)
|
|
176
|
+
except Exception:
|
|
177
|
+
handle_exception('in set_current')
|
|
178
|
+
|
|
179
|
+
def set_enable(value, *_):
|
|
180
|
+
try:
|
|
181
|
+
on = str(value).upper() in ('1', 'ON', 'TRUE')
|
|
182
|
+
cmd = 'MON' if on else 'MOFF'
|
|
183
|
+
reply = _send(cmd, updateStatus=True)
|
|
184
|
+
if not _is_ack(reply):
|
|
185
|
+
raise RuntimeError(f'Unexpected reply for {cmd}: {reply}')
|
|
186
|
+
edev.publish('Enable', 1 if on else 0, ifChanged=True)
|
|
187
|
+
except Exception:
|
|
188
|
+
handle_exception('in set_enable')
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def set_status_reset(value, *_):
|
|
192
|
+
try:
|
|
193
|
+
v = str(value).upper()
|
|
194
|
+
if v in ('1', 'ON', 'TRUE'):
|
|
195
|
+
reply = _send('MRESET', updateStatus=True)
|
|
196
|
+
if not _is_ack(reply):
|
|
197
|
+
raise RuntimeError(f'Unexpected reply for MRESET: {reply}')
|
|
198
|
+
edev.publish('StatusReset', 0, ifChanged=True)
|
|
199
|
+
except Exception:
|
|
200
|
+
handle_exception('in set_status_reset')
|
|
201
|
+
|
|
202
|
+
def set_instrCmdS(cmd, *_):
|
|
203
|
+
try:
|
|
204
|
+
text = str(cmd).strip()
|
|
205
|
+
if text == '':
|
|
206
|
+
return
|
|
207
|
+
reply = _send(text, updateStatus=True)
|
|
208
|
+
edev.publish('instrCmdR', reply)
|
|
209
|
+
except Exception:
|
|
210
|
+
handle_exception('in set_instrCmdS')
|
|
211
|
+
|
|
212
|
+
def set_upmode(value, *_):
|
|
213
|
+
try:
|
|
214
|
+
mode = str(value).strip().upper()
|
|
215
|
+
if mode not in ('NORMAL', 'ANALOG', 'WAVEFORM', 'SFP'):
|
|
216
|
+
raise ValueError(f'Invalid update mode: {mode}')
|
|
217
|
+
reply = _send(f'UPMODE:{mode}', updateStatus=True)
|
|
218
|
+
if not _is_ack(reply):
|
|
219
|
+
raise RuntimeError(f'Unexpected reply for UPMODE: {reply}')
|
|
220
|
+
edev.publish('Upmode', mode, ifChanged=True)
|
|
221
|
+
except Exception:
|
|
222
|
+
handle_exception('in set_upmode')
|
|
223
|
+
|
|
224
|
+
def set_rampV_rate(value, *_):
|
|
225
|
+
try:
|
|
226
|
+
rate = float(value)
|
|
227
|
+
reply = _send(f'MSRV:{rate}', updateStatus=True)
|
|
228
|
+
if not _is_ack(reply):
|
|
229
|
+
raise RuntimeError(f'Unexpected reply for MSRV: {reply}')
|
|
230
|
+
edev.publish('RampRateV', rate, ifChanged=True)
|
|
231
|
+
except Exception:
|
|
232
|
+
handle_exception('in set_rampV_rate')
|
|
233
|
+
|
|
234
|
+
def set_rampI_rate(value, *_):
|
|
235
|
+
try:
|
|
236
|
+
rate = float(value)
|
|
237
|
+
reply = _send(f'MSRI:{rate}', updateStatus=True)
|
|
238
|
+
if not _is_ack(reply):
|
|
239
|
+
raise RuntimeError(f'Unexpected reply for MSRI: {reply}')
|
|
240
|
+
edev.publish('RampRateI', rate, ifChanged=True)
|
|
241
|
+
except Exception:
|
|
242
|
+
handle_exception('in set_rampI_rate')
|
|
243
|
+
|
|
244
|
+
def myPVDefs():
|
|
245
|
+
"""PV definitions similar to ioc/fastps.db records."""
|
|
246
|
+
F, T, U, LL, LH, SET = 'features', 'type', 'units', 'limitLow', 'limitHigh', 'setter'
|
|
247
|
+
|
|
248
|
+
pv_defs = [
|
|
249
|
+
['dateTime', 'Server local date/time', 'N/A'],
|
|
250
|
+
['host', 'FAST-PS host', pargs.host],
|
|
251
|
+
['port', 'FAST-PS TCP port', pargs.port, {T: 'u32'}],
|
|
252
|
+
['RegulationMode', 'Selects between voltage/current regulation', ['V', 'I'], {F: 'WD', SET: set_regulation_mode}],
|
|
253
|
+
['Voltage', 'Voltage control (V regulation mode)', 0.0, {F: 'W', U: 'V', SET: set_voltage}],
|
|
254
|
+
['Current', 'Current control (I regulation mode)', 0.0, {F: 'W', U: 'A', SET: set_current}],
|
|
255
|
+
['StatusReset', 'Reset status register / clear faults', 0, {F: 'W', T: 'u8', LL: 0, LH: 1, SET: set_status_reset}],
|
|
256
|
+
['RampEnable', 'Enable/disable ramp to setpoint', ['Off', 'On'], {F: 'WD'}],
|
|
257
|
+
['OutputVoltage', 'Output voltage', 0.0, {U: 'V'}],
|
|
258
|
+
['OutputCurrent', 'Output current', 0.0, {U: 'A'}],
|
|
259
|
+
['GroundCurrent', 'Ground current', 0.0, {U: 'A'}],
|
|
260
|
+
['DCLinkVoltage', 'DC link voltage', 0.0, {U: 'V'}],
|
|
261
|
+
['HeatsinkTemp', 'Heatsink temperature', 0.0, {U: 'C'}],
|
|
262
|
+
['StatusMSB', 'Status MSB', 0, {T: 'u32'}],
|
|
263
|
+
['StatusLSB', 'Status LSB', 0, {T: 'u32'}],
|
|
264
|
+
['Limits', 'Voltage/current limits [MinV, MaxV, MinI, MaxI]', [0.0, 0.0, 0.0, 0.0]],
|
|
265
|
+
['Model', 'Power supply model', 'N/A'],
|
|
266
|
+
['Version', 'Power supply firmware version', 'N/A'],
|
|
267
|
+
['Enable', 'Turn supply off/on', ['Off', 'On'], {F: 'WD', SET: set_enable}],
|
|
268
|
+
['instrCmdS', 'Execute custom FAST-PS command', 'VER', {F: 'W', SET: set_instrCmdS}],
|
|
269
|
+
['instrCmdR', 'Reply to custom FAST-PS command', ''],
|
|
270
|
+
['ReadbackPoll_.SCAN', 'Readback polling period',# it is not necessary because the polling period is defined by 'sleep' PV, it is kept for compatibility with original IOC
|
|
271
|
+
['1.0','0.5','0.2','0.1','0.01','2','5','10'], {F: 'WD', U: 's',
|
|
272
|
+
SET: lambda v, *_: edev.publish('sleep', float(v), ifChanged=True)}],
|
|
273
|
+
['Upmode', 'Update mode', ['NORMAL', 'ANALOG', 'WAVEFORM', 'SFP'], {F: 'WD', SET: set_upmode}],
|
|
274
|
+
['RampRateV', 'Ramp rate V/s', 1.0, {F: 'W', U: 'V/s', LL:1e-6, LH:500.0, SET:set_rampV_rate}],
|
|
275
|
+
['RampRateI', 'Ramp rate I/s', 1.0, {F: 'W', U: 'A/s', LL:1e-6, LH:500.0, SET:set_rampI_rate}],
|
|
276
|
+
]
|
|
277
|
+
return pv_defs
|
|
278
|
+
|
|
279
|
+
def refresh_static():
|
|
280
|
+
"""Read static identification and setpoint values."""
|
|
281
|
+
ver = _query_text('VER', 'N/A')
|
|
282
|
+
C_.model = ver.split(':')[1]
|
|
283
|
+
edev.publish('Model', C_.model)
|
|
284
|
+
fw = ver.rsplit(':',1)[-1]
|
|
285
|
+
edev.publish('Version', fw)
|
|
286
|
+
edev.publish('Limits', SetpointLimits[C_.model])
|
|
287
|
+
|
|
288
|
+
loop = _query_text('LOOP ?', 'V').upper()
|
|
289
|
+
edev.publish('RegulationMode', 'I' if 'I' in loop else 'V', ifChanged=True)
|
|
290
|
+
|
|
291
|
+
edev.publish('Voltage', _query_float('MWV ?', 0.0), ifChanged=True)
|
|
292
|
+
edev.publish('Current', _query_float('MWI ?', 0.0), ifChanged=True)
|
|
293
|
+
|
|
294
|
+
def poll():
|
|
295
|
+
"""Main polling hook."""
|
|
296
|
+
edev.publish('OutputVoltage', _query_float('MRV', edev.pvv('OutputVoltage')), ifChanged=True)
|
|
297
|
+
edev.publish('OutputCurrent', _query_float('MRI', edev.pvv('OutputCurrent')), ifChanged=True)
|
|
298
|
+
edev.publish('GroundCurrent', _query_float('MGC', edev.pvv('GroundCurrent')), ifChanged=True)
|
|
299
|
+
edev.publish('DCLinkVoltage', _query_float('MRP', edev.pvv('DCLinkVoltage')), ifChanged=True)
|
|
300
|
+
edev.publish('HeatsinkTemp', _query_float('MRT', edev.pvv('HeatsinkTemp')), ifChanged=True)
|
|
301
|
+
|
|
302
|
+
def periodic_update():
|
|
303
|
+
"""Slow periodic update hook."""
|
|
304
|
+
#print('Periodic update')
|
|
305
|
+
edev.publish('dateTime', time.strftime('%Y-%m-%d %H:%M:%S'), ifChanged=True)
|
|
306
|
+
_query_status()
|
|
307
|
+
|
|
308
|
+
def serverStateChanged(newState: str):
|
|
309
|
+
"""Callback for server state transitions."""
|
|
310
|
+
if newState == 'Start':
|
|
311
|
+
edev.printi('Start requested')
|
|
312
|
+
refresh_static()
|
|
313
|
+
_query_status()
|
|
314
|
+
elif newState == 'Stop':
|
|
315
|
+
edev.printi('Stop requested')
|
|
316
|
+
elif newState == 'Exit':
|
|
317
|
+
edev.printi('Exit requested')
|
|
318
|
+
|
|
319
|
+
if __name__ == '__main__':
|
|
320
|
+
parser = argparse.ArgumentParser(
|
|
321
|
+
description=__doc__,
|
|
322
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
323
|
+
epilog=__version__,
|
|
324
|
+
)
|
|
325
|
+
parser.add_argument('-a', '--autosave', nargs='?', default='', help='Autosave control. If omitted, autosave is enabled with default directory.')
|
|
326
|
+
parser.add_argument('-c', '--recall', action='store_false', help='If given: do not restore initial PV values from autosave cache.')
|
|
327
|
+
parser.add_argument('-d', '--device', default='caen_fastps', help='Device name, the PV prefix is <device><index>:')
|
|
328
|
+
parser.add_argument('-i', '--index', default='0', help='Device index, the PV prefix is <device><index>:')
|
|
329
|
+
parser.add_argument('-p', '--putlogPV', nargs='?', default='', help='PV name for logging put operations. Empty means default putlog:dump.')
|
|
330
|
+
parser.add_argument('-v', '--verbose', action='count', default=0, help='Increase verbosity (-vv for more).')
|
|
331
|
+
|
|
332
|
+
parser.add_argument('--host', default=DEFAULT_HOST, help='FAST-PS host name or IP address')
|
|
333
|
+
parser.add_argument('--port', type=int, default=DEFAULT_PORT, help='FAST-PS TCP port')
|
|
334
|
+
parser.add_argument('--timeout', type=float, default=DEFAULT_TIMEOUT, help='TCP timeout in seconds')
|
|
335
|
+
|
|
336
|
+
pargs = parser.parse_args()
|
|
337
|
+
if pargs.putlogPV == '':
|
|
338
|
+
pargs.putlogPV = 'putlog:dump'
|
|
339
|
+
pargs.prefix = f'{pargs.device}{pargs.index}:'
|
|
340
|
+
|
|
341
|
+
_connect()
|
|
342
|
+
C_.PvDefs = myPVDefs()
|
|
343
|
+
|
|
344
|
+
PVs = edev.init_epicsdev(
|
|
345
|
+
pargs.prefix,
|
|
346
|
+
C_.PvDefs,
|
|
347
|
+
pargs.verbose,
|
|
348
|
+
serverStateChanged,
|
|
349
|
+
'',
|
|
350
|
+
pargs.autosave,
|
|
351
|
+
pargs.recall,
|
|
352
|
+
pargs.putlogPV,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
edev.publish('VERSION', __version__)
|
|
356
|
+
edev.set_server('Start')
|
|
357
|
+
|
|
358
|
+
server = edev.Server(providers=[PVs])
|
|
359
|
+
edev.printi(f'Server for {pargs.prefix} started. Sleeping per cycle: {repr(edev.pvv("sleep"))} S.')
|
|
360
|
+
while True:
|
|
361
|
+
state = edev.serverState()
|
|
362
|
+
if state.startswith('Exit'):
|
|
363
|
+
break
|
|
364
|
+
if not state.startswith('Stop'):
|
|
365
|
+
poll()
|
|
366
|
+
if not edev.sleep():
|
|
367
|
+
periodic_update()
|
|
368
|
+
|
|
369
|
+
try:
|
|
370
|
+
if C_.sock is not None:
|
|
371
|
+
C_.sock.close()
|
|
372
|
+
except OSError:
|
|
373
|
+
pass
|
|
374
|
+
|
|
375
|
+
edev.printi('Server is exited')
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: epicsdev_ps_caen_fastps
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: EPICS PVAccess server for CAEN FAST-PS power supplies
|
|
5
|
+
Author-email: Andrey Sukhanov <sukhanov@bnl.gov>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: epicsdev>=1.0.2
|
|
12
|
+
Provides-Extra: opi
|
|
13
|
+
Requires-Dist: phoebusgen; extra == 'opi'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# epicsdev_ps_caen_fastps
|
|
17
|
+
|
|
18
|
+
EPICS PVAccess server for CAEN FAST-PS power supplies, implemented with `epicsdev`.
|
|
19
|
+
|
|
20
|
+
Main server module: [caen_fastps/__main__.py](caen_fastps/__main__.py)
|
|
21
|
+
IOC reference DB: [ioc/fastps.db](ioc/fastps.db)
|
|
22
|
+
Phoebus screen generator: [opi/generate_screen.py](screens/generate_screen.py)
|
|
23
|
+
- Main server module: [epicsdev_ps_caen_fastps/__main__.py](epicsdev_ps_caen_fastps/__main__.py)
|
|
24
|
+
- IOC reference DB: [docs/fastps.db](docs/fastps.db)
|
|
25
|
+
- Phoebus screen generator: [opi/generate_opi.py](opi/generate_opi.py)
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- TCP remote control interface to FAST-PS (default port `10001`)
|
|
30
|
+
- PV set modeled after the IOC records in [docs/fastps.db](docs/fastps.db)
|
|
31
|
+
- Setpoint control with optional ramping:
|
|
32
|
+
- `Voltage`, `Current`, `RampEnable`
|
|
33
|
+
- Output and diagnostic readback:
|
|
34
|
+
- `OutputVoltage`, `OutputCurrent`, `GroundCurrent`, `DCLinkVoltage`, `HeatsinkTemp`
|
|
35
|
+
- Status and state control:
|
|
36
|
+
- `StatusMSB`, `StatusLSB`, `StatusReset`, `Enable`, `RegulationMode`, `Upmode`
|
|
37
|
+
- Ramp-rate control:
|
|
38
|
+
- `RampRateV`, `RampRateI`
|
|
39
|
+
- Device identity and inferred limits:
|
|
40
|
+
- `Model`, `Version`, `Limits`
|
|
41
|
+
- Generic command passthrough:
|
|
42
|
+
- `instrCmdS`, `instrCmdR`
|
|
43
|
+
|
|
44
|
+
## FAST-PS protocol mapping
|
|
45
|
+
|
|
46
|
+
Implemented command families:
|
|
47
|
+
|
|
48
|
+
- `VER`
|
|
49
|
+
- `MON`, `MOFF`
|
|
50
|
+
- `LOOP`, `LOOP ?`
|
|
51
|
+
- `MWV`, `MWV ?`, `MWVR`
|
|
52
|
+
- `MWI`, `MWI ?`, `MWIR`
|
|
53
|
+
- `MRESET`
|
|
54
|
+
- `MST`
|
|
55
|
+
- `UPMODE:?`, `UPMODE:<mode>`
|
|
56
|
+
- `MSRV:?`, `MSRV:<value>`
|
|
57
|
+
- `MSRI:?`, `MSRI:<value>`
|
|
58
|
+
- `MRV`, `MRI`, `MGC`, `MRP`, `MRT`
|
|
59
|
+
|
|
60
|
+
## Requirements
|
|
61
|
+
|
|
62
|
+
- Python 3.10+
|
|
63
|
+
- `epicsdev` and its runtime dependencies (including `p4p`)
|
|
64
|
+
- Network access to the CAEN FAST-PS device
|
|
65
|
+
|
|
66
|
+
## Install and run
|
|
67
|
+
|
|
68
|
+
- `pip install epicsdev_ps_caen_fastps
|
|
69
|
+
- `python -m epicsdev_ps_caen_fastps`
|
|
70
|
+
|
|
71
|
+
Useful arguments:
|
|
72
|
+
|
|
73
|
+
- `--host` FAST-PS IP/hostname (default: `130.199.104.57`)
|
|
74
|
+
- `--port` TCP port (default: `10001`)
|
|
75
|
+
- `--timeout` socket timeout in seconds (default: `2.0`)
|
|
76
|
+
- `-d, --device` PV prefix device root (default: `caen_fastps`)
|
|
77
|
+
- `-i, --index` PV prefix index (default: `0`)
|
|
78
|
+
- `-v` increase verbosity (`-vv` for more)
|
|
79
|
+
- `-a, --autosave` enable autosave with optional directory
|
|
80
|
+
- `-c, --recall` disable restoring autosaved values on startup
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
|
|
84
|
+
- `python -m epicsdev_ps_caen_fastps --host 130.199.104.57 -d caen_fastps:`
|
|
85
|
+
|
|
86
|
+
Default PV prefix:
|
|
87
|
+
|
|
88
|
+
- `caen_fastps0:`
|
|
89
|
+
|
|
90
|
+
## Generate a Phoebus screen
|
|
91
|
+
|
|
92
|
+
Generate `.bob` OPI file:
|
|
93
|
+
|
|
94
|
+
- `python generate_opi.py -t FAST-PS pva://caen_fastps:0:`
|
|
95
|
+
|
|
96
|
+
Options:
|
|
97
|
+
|
|
98
|
+
- `-t, --title` screen title
|
|
99
|
+
- `prefix` PV prefix macro/value (default: `$(DEV):`)
|
|
100
|
+
|
|
101
|
+
Output:
|
|
102
|
+
|
|
103
|
+
- [opi/caen_fastps.bob](opi/caen_fastps.bob)
|
|
104
|
+
|
|
105
|
+
## Screenshot
|
|
106
|
+
|
|
107
|
+
- [docs/opi_caen_fastps.jpg](docs/opi_caen_fastps.jpg)
|
|
108
|
+
|
|
109
|
+
## Notes
|
|
110
|
+
|
|
111
|
+
- The server uses common `epicsdev` control PVs such as `server`, `sleep`, `status`, and `HEARTBEAT`.
|
|
112
|
+
- `Enable` is synchronized from bit 0 of `StatusLSB` during polling.
|
|
113
|
+
- For unsupported/custom diagnostics, use `instrCmdS` and read the reply from `instrCmdR`.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
epicsdev_ps_caen_fastps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
epicsdev_ps_caen_fastps/__main__.py,sha256=eZ_0wNLz6Vl30Tt_t8E9MzXO_Ar223qvEGUUEzkcmDw,14394
|
|
3
|
+
epicsdev_ps_caen_fastps-0.0.3.dist-info/METADATA,sha256=OPQ9dmvVRO3l_ozlTDI0m0StYnLDYlKE5DZCNtl290I,3404
|
|
4
|
+
epicsdev_ps_caen_fastps-0.0.3.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
5
|
+
epicsdev_ps_caen_fastps-0.0.3.dist-info/licenses/LICENSE,sha256=b05363Mlkgmk87PGUA5VCSerlC-TK2Q1GaP3o33qCLY,1508
|
|
6
|
+
epicsdev_ps_caen_fastps-0.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Electron-Ion Collider
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|