mmcb-rs232-avt 1.0.20__py3-none-any.whl → 1.1.37__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.
mmcbrs232/mdcliapi.py ADDED
@@ -0,0 +1,110 @@
1
+ """Majordomo Protocol Client API, Python version.
2
+
3
+ Implements the MDP/Worker spec at http:#rfc.zeromq.org/spec:7.
4
+
5
+ Author: Min RK <benjaminrk@gmail.com>
6
+ Based on Java example by Arkadiusz Orzechowski
7
+ """
8
+
9
+ import logging
10
+
11
+ import zmq
12
+
13
+ from mmcbrs232 import MDP
14
+ from mmcbrs232 import zhelpers
15
+
16
+ class MajorDomoClient(object):
17
+ """Majordomo Protocol Client API, Python version.
18
+
19
+ Implements the MDP/Worker spec at http:#rfc.zeromq.org/spec:7.
20
+ """
21
+ broker = None
22
+ ctx = None
23
+ client = None
24
+ poller = None
25
+ # ms
26
+ timeout = 20000
27
+ retries = 5
28
+ verbose = False
29
+
30
+ def __init__(self, broker, verbose=False):
31
+ self.broker = broker
32
+ self.verbose = verbose
33
+ self.ctx = zmq.Context()
34
+ self.poller = zmq.Poller()
35
+ logging.basicConfig(format="%(asctime)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S",
36
+ level=logging.INFO)
37
+ self.reconnect_to_broker()
38
+
39
+
40
+ def reconnect_to_broker(self):
41
+ """Connect or reconnect to broker"""
42
+ if self.client:
43
+ self.poller.unregister(self.client)
44
+ self.client.close()
45
+ self.client = self.ctx.socket(zmq.REQ)
46
+
47
+ self.client.setsockopt(zmq.SNDHWM, 1000)
48
+ self.client.setsockopt(zmq.SNDBUF, 1000)
49
+ self.client.setsockopt(zmq.RCVHWM, 1000)
50
+ self.client.setsockopt(zmq.RCVBUF, 1000)
51
+ self.client.set_hwm(10000)
52
+
53
+ self.client.linger = 0
54
+ self.client.connect(self.broker)
55
+ self.poller.register(self.client, zmq.POLLIN)
56
+ if self.verbose:
57
+ logging.info("I: connecting to broker at %s...", self.broker)
58
+
59
+ def send(self, service, request):
60
+ """Send request to broker and get reply by hook or crook.
61
+
62
+ Takes ownership of request message and destroys it when sent.
63
+ Returns the reply message or None if there was no reply.
64
+ """
65
+ if not isinstance(request, list):
66
+ request = [request]
67
+ request = [MDP.C_CLIENT, service] + request
68
+ if self.verbose:
69
+ logging.warn("I: send request to '%s' service: ", service)
70
+ zhelpers.dump(request)
71
+ reply = None
72
+
73
+ retries = self.retries
74
+ while retries > 0:
75
+ self.client.send_multipart(request)
76
+ try:
77
+ items = self.poller.poll(self.timeout)
78
+ except KeyboardInterrupt:
79
+ break # interrupted
80
+
81
+ if items:
82
+ msg = self.client.recv_multipart()
83
+ if self.verbose:
84
+ logging.info("I: received reply:")
85
+ zhelpers.dump(msg)
86
+
87
+ # Don't try to handle errors, just assert noisily
88
+ assert len(msg) >= 3
89
+
90
+ header = msg.pop(0)
91
+ assert MDP.C_CLIENT == header
92
+
93
+ reply_service = msg.pop(0)
94
+ assert service == reply_service
95
+
96
+ reply = msg
97
+ break
98
+ else:
99
+ if retries:
100
+ logging.warn("W: no reply, reconnecting...")
101
+ self.reconnect_to_broker()
102
+ else:
103
+ logging.warn("W: permanent error, abandoning")
104
+ break
105
+ retries -= 1
106
+
107
+ return reply
108
+
109
+ def destroy(self):
110
+ self.context.destroy()
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Client library to help integrate the ZeroMQ serial port server into the PID
4
+ test scripts.
5
+ """
6
+
7
+ import contextlib
8
+ import logging
9
+ import platform
10
+
11
+ try:
12
+ from importlib import metadata
13
+ except (ImportError, ModuleNotFoundError):
14
+ try:
15
+ import importlib_metadata as metadata
16
+ except (ImportError, ModuleNotFoundError):
17
+ py_version = platform.python_version()
18
+ if py_version < '3.8':
19
+ logging.warning(
20
+ 'Cannot import importlib, version information will NOT be available.'
21
+ )
22
+ logging.warning(
23
+ 'Unsupported Python version (%s), upgrade to Python 3.8 or newer.',
24
+ py_version,
25
+ )
26
+ else:
27
+ logging.error('Cannot import importlib')
28
+
29
+ from mmcbrs232 import mdcliapi
30
+ from mmcbrs232 import common
31
+ from mmcbrs232 import psuset
32
+ from mmcbrs232 import psustat
33
+
34
+ # read the package version number as originally specified in setup.py
35
+ try:
36
+ __version__ = metadata.version('mmcb-rs232-avt')
37
+ except NameError:
38
+ # end-user is probably using a Python version < 3.8
39
+ __version__ = 'unknown'
40
+
41
+
42
+ ###############################################################################
43
+ # support
44
+ ###############################################################################
45
+
46
+
47
+ def default():
48
+ """
49
+ ---------------------------------------------------------------------------
50
+ args : none
51
+ ---------------------------------------------------------------------------
52
+ returns : dict
53
+ ---------------------------------------------------------------------------
54
+ """
55
+ return {
56
+ 'alias': None,
57
+ 'channel': None,
58
+ 'current_limit': None,
59
+ 'debug': None,
60
+ 'decimal_places': 2,
61
+ 'forwardbias': False,
62
+ 'immediate': False,
63
+ 'manufacturer': None,
64
+ 'model': None,
65
+ 'on': None,
66
+ 'peltier': False,
67
+ 'port': None,
68
+ 'quiet': False,
69
+ 'rear': None,
70
+ 'reset': False,
71
+ 'serial': None,
72
+ 'settlingtime': 0.5,
73
+ 'verbose': None,
74
+ 'voltage': None,
75
+ 'voltspersecond': 10
76
+ }
77
+
78
+
79
+ ###############################################################################
80
+ # data structures
81
+ #
82
+ # model command lines from the PID scripts
83
+ ###############################################################################
84
+
85
+
86
+ class Rs232:
87
+ """
88
+ Handle serial port interactions
89
+ """
90
+
91
+ # read hardware configuration
92
+ psus = common.cache_read(['hvpsu', 'lvpsu', 'chiller'])
93
+ settings = default()
94
+ channels = common.ports_to_channels(settings, psus)
95
+ ports = sorted(psus)
96
+
97
+ # create client
98
+ client = mdcliapi.MajorDomoClient('tcp://localhost:5556', verbose=False)
99
+
100
+ ############################################################################
101
+ # support
102
+ ############################################################################
103
+
104
+ def _get_serial_port_name(self, command):
105
+ """
106
+ get serial port name from command (psuset, psustat, ult80)
107
+ """
108
+ local_psus = self.psus.copy()
109
+ local_channels = self.channels.copy()
110
+
111
+ cli = command.split(' ')[0].strip()
112
+
113
+ if cli == 'ult80':
114
+ # obtain from device cache since there are no channels for the ULT80
115
+ rval = [
116
+ serial_port_name
117
+ for serial_port_name, details in self.psus.items()
118
+ if details[1] == 'chiller'
119
+ ][0]
120
+
121
+ elif cli == 'psuset':
122
+ settings = default()
123
+ psuset.check_arguments(settings, command)
124
+ single = common.unique(settings, local_psus, local_channels)
125
+
126
+ rval = next(iter(local_psus)) if single else None
127
+
128
+ elif cli == 'psustat':
129
+ settings = {
130
+ 'alias': None,
131
+ 'channel': None,
132
+ 'manufacturer': None,
133
+ 'model': None,
134
+ 'port': None,
135
+ 'serial': None,
136
+ 'time': None,
137
+ }
138
+ psustat.check_arguments(settings, command)
139
+ single = common.unique(settings, local_psus, local_channels)
140
+
141
+ if settings['filter'] and not single:
142
+ rval = None
143
+ else:
144
+ rval = next(iter(local_psus))
145
+ else:
146
+ rval = None
147
+
148
+ return rval
149
+
150
+ def _issue_serial_command(self, command):
151
+ """
152
+ """
153
+ serial_port_name = self._get_serial_port_name(command)
154
+ reply = None
155
+ if serial_port_name is None:
156
+ return reply
157
+
158
+ request = bytes(command, encoding='utf-8')
159
+
160
+ with contextlib.suppress(KeyboardInterrupt):
161
+ reply = self.client.send(
162
+ bytes(serial_port_name, encoding='utf-8'), request
163
+ )
164
+
165
+ return reply
166
+
167
+ @staticmethod
168
+ def _reply_value(reply):
169
+ """
170
+ Extract value from returned string; discard latency figure.
171
+
172
+ ------------------------------------------------------------------------
173
+ args
174
+ reply : list of str
175
+ e.g. [b'ult80 -i : None (1.008616 s)']
176
+ ------------------------------------------------------------------------
177
+ returns : float, bool or None
178
+ ------------------------------------------------------------------------
179
+ """
180
+ value_str, *_ = reply[0].decode().partition(':')[-1].partition('(')
181
+
182
+ try:
183
+ value = float(value_str)
184
+ except ValueError:
185
+ value = {'True': True, 'False': False}.get(value_str.strip())
186
+
187
+ return value
188
+
189
+
190
+ ############################################################################
191
+ # obtain readings
192
+ ############################################################################
193
+
194
+ def get_ult80_temps(self):
195
+ """
196
+ ------------------------------------------------------------------------
197
+ args : none
198
+ ------------------------------------------------------------------------
199
+ returns : float, float
200
+ (internal_temperature, set point)
201
+ ------------------------------------------------------------------------
202
+ """
203
+ return (
204
+ self._reply_value(self._issue_serial_command('ult80 -r')),
205
+ self._reply_value(self._issue_serial_command('ult80 -i')),
206
+ )
207
+
208
+ def get_pt_v(self, psu_id, channel):
209
+ """
210
+ HMP4040 set voltage
211
+ """
212
+ return self._reply_value(
213
+ self._issue_serial_command(
214
+ f'psustat --model hmp4040 --channel {channel} --serial {psu_id} --sv'
215
+ )
216
+ )
217
+
218
+ def get_pt_i(self, psu_id, channel):
219
+ """
220
+ HMP4040 measured current
221
+ """
222
+ return self._reply_value(
223
+ self._issue_serial_command(
224
+ f'psustat --model hmp4040 --channel {channel} --serial {psu_id} --mi'
225
+ )
226
+ )
227
+
228
+ def get_pt_sv_fast(self, psu_id, channel):
229
+ """
230
+ HMP4040 get channel set voltage, tailored for ZeroMQ serial port server.
231
+ """
232
+ return self._reply_value(
233
+ self._issue_serial_command(
234
+ f'psustat --model hmp4040 --channel {channel} --serial {psu_id} --sv --zserv'
235
+ )
236
+ )
237
+
238
+ def get_pt_mi_fast(self, psu_id, channel):
239
+ """
240
+ HMP4040 get channel measured current, tailored for ZeroMQ serial port server.
241
+ """
242
+ return self._reply_value(
243
+ self._issue_serial_command(
244
+ f'psustat --model hmp4040 --channel {channel} --serial {psu_id} --mi --zserv'
245
+ )
246
+ )
247
+
248
+ def psu_set(self, psu_id, channel, voltage, current):
249
+ """
250
+ channel on
251
+ """
252
+ return self._reply_value(
253
+ self._issue_serial_command(
254
+ f'psuset {voltage} --limit {current} --on --channel {channel} --serial {psu_id}'
255
+ )
256
+ )
257
+
258
+ def psu_unset(self, psu_id, channel, voltage, current):
259
+ """
260
+ channel off
261
+ """
262
+ return self._reply_value(
263
+ self._issue_serial_command(
264
+ f'psuset {voltage} --limit {current} --off --channel {channel} --serial {psu_id}'
265
+ )
266
+ )
mmcbrs232/mdwrkapi.py ADDED
@@ -0,0 +1,183 @@
1
+ """Majordomo Protocol Worker API, Python version
2
+
3
+ Implements the MDP/Worker spec at http:#rfc.zeromq.org/spec:7.
4
+
5
+ Author: Min RK <benjaminrk@gmail.com>
6
+ Based on Java example by Arkadiusz Orzechowski
7
+ """
8
+
9
+ import logging
10
+ import time
11
+ import zmq
12
+
13
+ from mmcbrs232 import MDP
14
+ from mmcbrs232 import zhelpers
15
+
16
+ class MajorDomoWorker(object):
17
+ """Majordomo Protocol Worker API, Python version
18
+
19
+ Implements the MDP/Worker spec at http:#rfc.zeromq.org/spec:7.
20
+ """
21
+
22
+ # HEARTBEAT_LIVENESS = 3 # 3-5 is reasonable
23
+ HEARTBEAT_LIVENESS = 5 # 3-5 is reasonable
24
+
25
+ broker = None
26
+ ctx = None
27
+ service = None
28
+
29
+ worker = None # Socket to broker
30
+ heartbeat_at = 0 # When to send HEARTBEAT (relative to time.time(), so in seconds)
31
+ liveness = 0 # How many attempts left
32
+ heartbeat = 2500 # Heartbeat delay, msecs
33
+ reconnect = 2500 # Reconnect delay, msecs
34
+ # heartbeat = 5000 # Heartbeat delay, msecs
35
+ # reconnect = 5000 # Reconnect delay, msecs
36
+
37
+ # Internal state
38
+ expect_reply = False # False only at start
39
+
40
+ # timeout = 2500 # poller timeout
41
+ timeout = 5000 # poller timeout
42
+
43
+ verbose = False # Print activity to stdout
44
+
45
+ # Return address, if any
46
+ reply_to = None
47
+
48
+ def __init__(self, broker, service, verbose=False):
49
+ self.broker = broker
50
+ self.service = service
51
+ self.verbose = verbose
52
+ self.ctx = zmq.Context()
53
+ self.poller = zmq.Poller()
54
+ logging.basicConfig(format="%(asctime)s %(message)s",
55
+ datefmt="%Y-%m-%d %H:%M:%S",
56
+ level=logging.INFO)
57
+ self.reconnect_to_broker()
58
+
59
+
60
+ def reconnect_to_broker(self):
61
+ """Connect or reconnect to broker"""
62
+ if self.worker:
63
+ self.poller.unregister(self.worker)
64
+ self.worker.close()
65
+ self.worker = self.ctx.socket(zmq.DEALER)
66
+
67
+ self.worker.setsockopt(zmq.SNDHWM, 1000)
68
+ self.worker.setsockopt(zmq.SNDBUF, 1000)
69
+ self.worker.setsockopt(zmq.RCVHWM, 1000)
70
+ self.worker.setsockopt(zmq.RCVBUF, 1000)
71
+ self.worker.set_hwm(10000)
72
+
73
+ self.worker.linger = 0
74
+ self.worker.connect(self.broker)
75
+ self.poller.register(self.worker, zmq.POLLIN)
76
+ if self.verbose:
77
+ logging.info("I: connecting to broker at %s...", self.broker)
78
+
79
+ # Register service with broker
80
+ self.send_to_broker(MDP.W_READY, self.service, [])
81
+
82
+ # If liveness hits zero, queue is considered disconnected
83
+ self.liveness = self.HEARTBEAT_LIVENESS
84
+ self.heartbeat_at = time.time() + 1e-3 * self.heartbeat
85
+
86
+
87
+ def send_to_broker(self, command, option=None, msg=None):
88
+ """Send message to broker.
89
+
90
+ If no msg is provided, creates one internally
91
+ """
92
+ if msg is None:
93
+ msg = []
94
+ elif not isinstance(msg, list):
95
+ msg = [msg]
96
+
97
+ if option:
98
+ msg = [option] + msg
99
+
100
+ msg = [b'', MDP.W_WORKER, command] + msg
101
+ if self.verbose:
102
+ logging.info("I: sending %s to broker", command)
103
+ zhelpers.dump(msg)
104
+ self.worker.send_multipart(msg)
105
+
106
+
107
+ def recv(self, reply=None):
108
+ """Send reply, if any, to broker and wait for next request."""
109
+ # Format and send the reply if we were provided one
110
+ assert reply is not None or not self.expect_reply
111
+
112
+ if reply is not None:
113
+ assert self.reply_to is not None
114
+ reply = [self.reply_to, b''] + reply
115
+ self.send_to_broker(MDP.W_REPLY, msg=reply)
116
+
117
+ self.expect_reply = True
118
+
119
+ while True:
120
+ # Poll socket for a reply, with timeout
121
+ try:
122
+ items = self.poller.poll(self.timeout)
123
+ except KeyboardInterrupt:
124
+ break # Interrupted
125
+
126
+ if items:
127
+ msg = self.worker.recv_multipart()
128
+ if self.verbose:
129
+ logging.info("I: received message from broker: ")
130
+ zhelpers.dump(msg)
131
+
132
+ self.liveness = self.HEARTBEAT_LIVENESS
133
+ # Don't try to handle errors, just assert noisily
134
+ assert len(msg) >= 3
135
+
136
+ empty = msg.pop(0)
137
+ assert empty == b''
138
+
139
+ header = msg.pop(0)
140
+ assert header == MDP.W_WORKER
141
+
142
+ command = msg.pop(0)
143
+ if command == MDP.W_REQUEST:
144
+ # We should pop and save as many addresses as there are
145
+ # up to a null part, but for now, just save one...
146
+ self.reply_to = msg.pop(0)
147
+ # pop empty
148
+ empty = msg.pop(0)
149
+ assert empty == b''
150
+
151
+ return msg # We have a request to process
152
+ elif command == MDP.W_HEARTBEAT:
153
+ # Do nothing for heartbeats
154
+ pass
155
+ elif command == MDP.W_DISCONNECT:
156
+ self.reconnect_to_broker()
157
+ else :
158
+ logging.error("E: invalid input message: ")
159
+ zhelpers.dump(msg)
160
+
161
+ else:
162
+ self.liveness -= 1
163
+ if self.liveness == 0:
164
+ if self.verbose:
165
+ logging.warn("W: disconnected from broker - retrying...")
166
+ try:
167
+ time.sleep(1e-3*self.reconnect)
168
+ except KeyboardInterrupt:
169
+ break
170
+ self.reconnect_to_broker()
171
+
172
+ # Send HEARTBEAT if it's time
173
+ if time.time() > self.heartbeat_at:
174
+ self.send_to_broker(MDP.W_HEARTBEAT)
175
+ self.heartbeat_at = time.time() + 1e-3*self.heartbeat
176
+
177
+ logging.warn("W: interrupt received, killing worker...")
178
+ return None
179
+
180
+
181
+ def destroy(self):
182
+ # context.destroy depends on pyzmq >= 2.1.10
183
+ self.ctx.destroy(0)