symetrie-hexapod 0.17.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.
- egse/hexapod/__init__.py +33 -0
- egse/hexapod/symetrie/__init__.py +182 -0
- egse/hexapod/symetrie/alpha.py +857 -0
- egse/hexapod/symetrie/dynalpha.py +1438 -0
- egse/hexapod/symetrie/hexapod.py +550 -0
- egse/hexapod/symetrie/hexapod_ui.py +1484 -0
- egse/hexapod/symetrie/joran.py +289 -0
- egse/hexapod/symetrie/joran.yaml +62 -0
- egse/hexapod/symetrie/joran_cs.py +179 -0
- egse/hexapod/symetrie/joran_protocol.py +117 -0
- egse/hexapod/symetrie/joran_ui.py +433 -0
- egse/hexapod/symetrie/pmac.py +1001 -0
- egse/hexapod/symetrie/pmac_regex.py +86 -0
- egse/hexapod/symetrie/puna.py +649 -0
- egse/hexapod/symetrie/puna.yaml +193 -0
- egse/hexapod/symetrie/puna_cs.py +241 -0
- egse/hexapod/symetrie/puna_protocol.py +126 -0
- egse/hexapod/symetrie/puna_ui.py +410 -0
- egse/hexapod/symetrie/punaplus.py +115 -0
- egse/hexapod/symetrie/zonda.py +846 -0
- egse/hexapod/symetrie/zonda.yaml +337 -0
- egse/hexapod/symetrie/zonda_cs.py +239 -0
- egse/hexapod/symetrie/zonda_devif.py +416 -0
- egse/hexapod/symetrie/zonda_protocol.py +118 -0
- egse/hexapod/symetrie/zonda_ui.py +434 -0
- symetrie_hexapod/__init__.py +0 -0
- symetrie_hexapod/cgse_explore.py +19 -0
- symetrie_hexapod/cgse_services.py +162 -0
- symetrie_hexapod/settings.yaml +15 -0
- symetrie_hexapod-0.17.3.dist-info/METADATA +21 -0
- symetrie_hexapod-0.17.3.dist-info/RECORD +33 -0
- symetrie_hexapod-0.17.3.dist-info/WHEEL +4 -0
- symetrie_hexapod-0.17.3.dist-info/entry_points.txt +23 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the device classes to be used to connect to and control the Hexapod JORAN from
|
|
3
|
+
Symétrie.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import math
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
from egse.hexapod.symetrie.alpha import AlphaPlusControllerInterface
|
|
12
|
+
from egse.hexapod.symetrie.dynalpha import AlphaPlusTelnetInterface, decode_validation_error
|
|
13
|
+
from egse.mixin import DynamicCommandMixin
|
|
14
|
+
from egse.proxy import DynamicProxy
|
|
15
|
+
from egse.settings import Settings
|
|
16
|
+
from egse.system import Timer
|
|
17
|
+
from egse.system import wait_until
|
|
18
|
+
from egse.zmq_ser import connect_address
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
JORAN_SETTINGS = Settings.load("JORAN Controller")
|
|
23
|
+
CTRL_SETTINGS = Settings.load("Hexapod JORAN Control Server")
|
|
24
|
+
DEVICE_SETTINGS = Settings.load(filename="joran.yaml")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class JoranInterface(AlphaPlusControllerInterface):
|
|
28
|
+
"""
|
|
29
|
+
Interface definition for the JoranController, the JoranProxy, and the JoranSimulator.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class JoranController(JoranInterface, DynamicCommandMixin):
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.hostname = {JORAN_SETTINGS.IP}
|
|
36
|
+
self.port = {JORAN_SETTINGS.PORT}
|
|
37
|
+
self.transport = self.hexapod = AlphaPlusTelnetInterface(self.hostname, self.port)
|
|
38
|
+
|
|
39
|
+
super.__init__()
|
|
40
|
+
|
|
41
|
+
def is_simulator(self):
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
def is_connected(self):
|
|
45
|
+
return self.hexapod.is_connected()
|
|
46
|
+
|
|
47
|
+
def connect(self):
|
|
48
|
+
self.hexapod.connect()
|
|
49
|
+
|
|
50
|
+
def disconnect(self):
|
|
51
|
+
self.hexapod.disconnect()
|
|
52
|
+
|
|
53
|
+
def reconnect(self):
|
|
54
|
+
if self.is_connected():
|
|
55
|
+
self.disconnect()
|
|
56
|
+
self.connect()
|
|
57
|
+
|
|
58
|
+
def reset(self, wait=True):
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
|
|
61
|
+
# def sequence(self):
|
|
62
|
+
# raise NotImplementedError
|
|
63
|
+
|
|
64
|
+
def set_virtual_homing(self, tx, ty, tz, rx, ry, rz):
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
|
|
67
|
+
def get_debug_info(self):
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
|
|
70
|
+
def jog(self, axis: int, inc: float) -> int:
|
|
71
|
+
raise NotImplementedError
|
|
72
|
+
|
|
73
|
+
def get_temperature(self):
|
|
74
|
+
raise NotImplementedError
|
|
75
|
+
|
|
76
|
+
def get_limits_state(self):
|
|
77
|
+
raise NotImplementedError
|
|
78
|
+
|
|
79
|
+
def machine_limit_enable(self, state):
|
|
80
|
+
raise NotImplementedError
|
|
81
|
+
|
|
82
|
+
def user_limit_set(self, *par):
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
|
|
85
|
+
def set_default(self):
|
|
86
|
+
raise NotImplementedError
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class JoranSimulator(JoranInterface):
|
|
90
|
+
"""
|
|
91
|
+
HexapodSimulator simulates the Symétrie Hexapod JORAN. The class is heavily based on the
|
|
92
|
+
ReferenceFrames in the `egse.coordinates` package.
|
|
93
|
+
|
|
94
|
+
The simulator implements the same methods as the HexapodController class which acts on the
|
|
95
|
+
real hardware controller in either simulation mode or with a real Hexapod JORAN connected.
|
|
96
|
+
|
|
97
|
+
Therefore, the HexapodSimulator can be used instead of the Hexapod class in test harnesses
|
|
98
|
+
and when the hardware is not available.
|
|
99
|
+
|
|
100
|
+
This class simulates all the movements and status of the Hexapod.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self):
|
|
104
|
+
super().__init__()
|
|
105
|
+
|
|
106
|
+
# Keep a record if the homing() command has been executed.
|
|
107
|
+
|
|
108
|
+
self.homing_done = False
|
|
109
|
+
self.control_loop = False
|
|
110
|
+
self._virtual_homing = False
|
|
111
|
+
self._virtual_homing_position = None
|
|
112
|
+
|
|
113
|
+
def is_simulator(self):
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
def connect(self):
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def reconnect(self):
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
def disconnect(self):
|
|
123
|
+
# TODO:
|
|
124
|
+
# Should I keep state in this class to check if it has been disconnected?
|
|
125
|
+
#
|
|
126
|
+
# TODO:
|
|
127
|
+
# What happens when I re-connect to this Simulator? Shall it be in Homing position or
|
|
128
|
+
# do I have to keep state via a persistence mechanism?
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
def is_connected(self):
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
def clear_error(self):
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
def homing(self):
|
|
138
|
+
self.goto_zero_position()
|
|
139
|
+
self.homing_done = True
|
|
140
|
+
self._virtual_homing = False
|
|
141
|
+
self._virtual_homing_position = None
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
def is_homing_done(self):
|
|
145
|
+
return self.homing_done
|
|
146
|
+
|
|
147
|
+
def activate_control_loop(self):
|
|
148
|
+
self.control_loop = True
|
|
149
|
+
return self.control_loop
|
|
150
|
+
|
|
151
|
+
def deactivate_control_loop(self):
|
|
152
|
+
self.control_loop = False
|
|
153
|
+
return self.control_loop
|
|
154
|
+
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class JoranProxy(DynamicProxy, JoranInterface):
|
|
159
|
+
"""The JoranProxy class is used to connect to the control server and send commands to the
|
|
160
|
+
Hexapod JORAN remotely."""
|
|
161
|
+
|
|
162
|
+
def __init__(
|
|
163
|
+
self,
|
|
164
|
+
protocol=CTRL_SETTINGS.PROTOCOL,
|
|
165
|
+
hostname=CTRL_SETTINGS.HOSTNAME,
|
|
166
|
+
port=CTRL_SETTINGS.COMMANDING_PORT,
|
|
167
|
+
):
|
|
168
|
+
"""
|
|
169
|
+
Args:
|
|
170
|
+
protocol: the transport protocol [default is taken from settings file]
|
|
171
|
+
hostname: location of the control server (IP address) [default is taken from settings
|
|
172
|
+
file]
|
|
173
|
+
port: TCP port on which the control server is listening for commands [default is
|
|
174
|
+
taken from settings file]
|
|
175
|
+
"""
|
|
176
|
+
super().__init__(connect_address(protocol, hostname, port))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
from rich import print as rp
|
|
181
|
+
|
|
182
|
+
joran = JoranController()
|
|
183
|
+
joran.connect()
|
|
184
|
+
|
|
185
|
+
with Timer("JoranController"):
|
|
186
|
+
rp(joran.info())
|
|
187
|
+
rp(joran.is_homing_done())
|
|
188
|
+
rp(joran.is_in_position())
|
|
189
|
+
rp(joran.activate_control_loop())
|
|
190
|
+
rp(joran.get_general_state())
|
|
191
|
+
rp(joran.get_actuator_state())
|
|
192
|
+
rp(joran.deactivate_control_loop())
|
|
193
|
+
rp(joran.get_general_state())
|
|
194
|
+
rp(joran.get_actuator_state())
|
|
195
|
+
rp(joran.stop())
|
|
196
|
+
rp(joran.get_limits_value(0))
|
|
197
|
+
rp(joran.get_limits_value(1))
|
|
198
|
+
rp(joran.check_absolute_movement(1, 1, 1, 1, 1, 1))
|
|
199
|
+
rp(joran.check_absolute_movement(51, 51, 51, 1, 1, 1))
|
|
200
|
+
rp(joran.get_speed())
|
|
201
|
+
rp(joran.set_speed(2.0, 1.0))
|
|
202
|
+
time.sleep(0.5) # if we do not sleep, the get_speed() will get the old values
|
|
203
|
+
speed = joran.get_speed()
|
|
204
|
+
|
|
205
|
+
if not math.isclose(speed["vt"], 2.0):
|
|
206
|
+
rp(f"[red]{speed['vt']} != 2.0[/red]")
|
|
207
|
+
if not math.isclose(speed["vr"], 1.0):
|
|
208
|
+
rp(f"[red]{speed['vr']} != 1.0[/red]")
|
|
209
|
+
|
|
210
|
+
rp(joran.get_actuator_length())
|
|
211
|
+
|
|
212
|
+
# rp(joran.machine_limit_enable(0))
|
|
213
|
+
# rp(joran.machine_limit_enable(1))
|
|
214
|
+
# rp(joran.get_limits_state())
|
|
215
|
+
rp(joran.get_coordinates_systems())
|
|
216
|
+
rp(
|
|
217
|
+
joran.configure_coordinates_systems(
|
|
218
|
+
0.033000,
|
|
219
|
+
-0.238000,
|
|
220
|
+
230.205000,
|
|
221
|
+
0.003282,
|
|
222
|
+
0.005671,
|
|
223
|
+
0.013930,
|
|
224
|
+
0.000000,
|
|
225
|
+
0.000000,
|
|
226
|
+
0.000000,
|
|
227
|
+
0.000000,
|
|
228
|
+
0.000000,
|
|
229
|
+
0.000000,
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
rp(joran.get_coordinates_systems())
|
|
233
|
+
rp(joran.get_machine_positions())
|
|
234
|
+
rp(joran.get_user_positions())
|
|
235
|
+
rp(
|
|
236
|
+
joran.configure_coordinates_systems(
|
|
237
|
+
0.000000,
|
|
238
|
+
0.000000,
|
|
239
|
+
0.000000,
|
|
240
|
+
0.000000,
|
|
241
|
+
0.000000,
|
|
242
|
+
0.000000,
|
|
243
|
+
0.000000,
|
|
244
|
+
0.000000,
|
|
245
|
+
0.000000,
|
|
246
|
+
0.000000,
|
|
247
|
+
0.000000,
|
|
248
|
+
0.000000,
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
rp(joran.validate_position(1, 0, 0, 0, 0, 0, 0, 0))
|
|
252
|
+
rp(joran.validate_position(1, 0, 0, 0, 50, 0, 0, 0))
|
|
253
|
+
|
|
254
|
+
rp(joran.goto_zero_position())
|
|
255
|
+
rp(joran.is_in_position())
|
|
256
|
+
if wait_until(joran.is_in_position, interval=1, timeout=300):
|
|
257
|
+
rp("[red]Task joran.is_in_position() timed out after 30s.[/red]")
|
|
258
|
+
rp(joran.is_in_position())
|
|
259
|
+
|
|
260
|
+
rp(joran.get_machine_positions())
|
|
261
|
+
rp(joran.get_user_positions())
|
|
262
|
+
|
|
263
|
+
rp(joran.move_absolute(0, 0, 12, 0, 0, 10))
|
|
264
|
+
|
|
265
|
+
rp(joran.is_in_position())
|
|
266
|
+
if wait_until(joran.is_in_position, interval=1, timeout=300):
|
|
267
|
+
rp("[red]Task joran.is_in_position() timed out after 30s.[/red]")
|
|
268
|
+
rp(joran.is_in_position())
|
|
269
|
+
|
|
270
|
+
rp(joran.get_machine_positions())
|
|
271
|
+
rp(joran.get_user_positions())
|
|
272
|
+
|
|
273
|
+
rp(joran.move_absolute(0, 0, 0, 0, 0, 0))
|
|
274
|
+
|
|
275
|
+
rp(joran.is_in_position())
|
|
276
|
+
if wait_until(joran.is_in_position, interval=1, timeout=300):
|
|
277
|
+
rp("[red]Task joran.is_in_position() timed out after 30s.[/red]")
|
|
278
|
+
rp(joran.is_in_position())
|
|
279
|
+
|
|
280
|
+
rp(joran.get_machine_positions())
|
|
281
|
+
rp(joran.get_user_positions())
|
|
282
|
+
|
|
283
|
+
# joran.reset()
|
|
284
|
+
joran.disconnect()
|
|
285
|
+
|
|
286
|
+
rp(0, decode_validation_error(0))
|
|
287
|
+
rp(11, decode_validation_error(11))
|
|
288
|
+
rp(8, decode_validation_error(8))
|
|
289
|
+
rp(24, decode_validation_error(24))
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
BaseClass:
|
|
2
|
+
egse.hexapod.symetrie.alpha.AlphaPlusControllerInterface
|
|
3
|
+
|
|
4
|
+
ProxyClass:
|
|
5
|
+
egse.hexapod.symetrie.joran.JoranProxy
|
|
6
|
+
|
|
7
|
+
ControlServerClass:
|
|
8
|
+
egse.hexapod.symetrie.joran_cs.JoranControlServer
|
|
9
|
+
|
|
10
|
+
ControlServer:
|
|
11
|
+
egse.hexapod.symetrie.joran_cs
|
|
12
|
+
|
|
13
|
+
UserInterface:
|
|
14
|
+
egse.hexapod.symetrie.joran_ui
|
|
15
|
+
|
|
16
|
+
Commands:
|
|
17
|
+
|
|
18
|
+
# Each of these groups is parsed and used on both the server and the client side.
|
|
19
|
+
#
|
|
20
|
+
# The group name (e.g. is_simulator) will be monkey patched in the Proxy class for the device
|
|
21
|
+
# or service.
|
|
22
|
+
#
|
|
23
|
+
# The other field are:
|
|
24
|
+
# description: Used by the doc_string method to generate a help string
|
|
25
|
+
# cmd: Command string that will eventually be sent to the hardware controller for
|
|
26
|
+
# the device. This cmd string is also used at the client side to parse and
|
|
27
|
+
# validate the arguments.
|
|
28
|
+
# device_method: The name of the method to be called on the device class.
|
|
29
|
+
# These should all be defined by the interface class for the device, i.e.
|
|
30
|
+
# JoranInterface in this case.
|
|
31
|
+
# When the device_method is the same as the group name, it can be omitted.
|
|
32
|
+
# response: The name of the method to be called from the device protocol.
|
|
33
|
+
# This method should exist in the subclass of the CommandProtocol base class,
|
|
34
|
+
# i.e. in this case it will be the JoranProtocol class.
|
|
35
|
+
# The default (when no response is given) is 'handle_device_method'.
|
|
36
|
+
|
|
37
|
+
# Definition of the DeviceInterface
|
|
38
|
+
|
|
39
|
+
is_simulator:
|
|
40
|
+
description: Ask if the connected class is a simulator instead of the real device Controller class.
|
|
41
|
+
returns: bool | True if the far end is a simulator instead of the real hardware
|
|
42
|
+
|
|
43
|
+
is_connected:
|
|
44
|
+
description: Check if the Hexapod hardware controller is connected.
|
|
45
|
+
|
|
46
|
+
connect:
|
|
47
|
+
description: Connect the Hexapod hardware controller
|
|
48
|
+
|
|
49
|
+
reconnect:
|
|
50
|
+
description: Reconnect the Hexapod hardware controller.
|
|
51
|
+
|
|
52
|
+
This command will force a disconnect and then try to re-connect to the controller.
|
|
53
|
+
|
|
54
|
+
disconnect:
|
|
55
|
+
description: Disconnect from the hexapod controller.
|
|
56
|
+
|
|
57
|
+
This command will be send to the Hexapod Control Server which will then
|
|
58
|
+
disconnect from the hardware controller.
|
|
59
|
+
|
|
60
|
+
This command does not affect the ZeroMQ connection of the Proxy to the
|
|
61
|
+
control server. Use the service command `disconnect_cs()` to disconnect
|
|
62
|
+
from the control server.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The Control Server that connects to the Hexapod JORAN Hardware Controller.
|
|
3
|
+
|
|
4
|
+
Start the control server from the terminal as follows:
|
|
5
|
+
|
|
6
|
+
$ joran_cs start-bg
|
|
7
|
+
|
|
8
|
+
or when you don't have the device available, start the control server in simulator mode. That
|
|
9
|
+
will make the control server connect to a device software simulator:
|
|
10
|
+
|
|
11
|
+
$ joran_cs start --sim
|
|
12
|
+
|
|
13
|
+
Please note that software simulators are intended for simple test purposes and will not simulate
|
|
14
|
+
all device behavior correctly, e.g. timing, error conditions, etc.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
from egse.process import SubProcess
|
|
21
|
+
|
|
22
|
+
if __name__ != "__main__":
|
|
23
|
+
import multiprocessing
|
|
24
|
+
|
|
25
|
+
multiprocessing.current_process().name = "joran_cs"
|
|
26
|
+
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
import click
|
|
30
|
+
import rich
|
|
31
|
+
import zmq
|
|
32
|
+
|
|
33
|
+
from egse.control import ControlServer
|
|
34
|
+
from egse.control import is_control_server_active
|
|
35
|
+
from egse.hexapod.symetrie.joran import JoranProxy
|
|
36
|
+
from egse.hexapod.symetrie.joran_protocol import JoranProtocol
|
|
37
|
+
from egse.settings import Settings
|
|
38
|
+
from egse.zmq_ser import connect_address
|
|
39
|
+
from prometheus_client import start_http_server
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
CTRL_SETTINGS = Settings.load("Hexapod JORAN Control Server")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class JoranControlServer(ControlServer):
|
|
47
|
+
"""JoranControlServer - Command and monitor the Hexapod JORAN hardware.
|
|
48
|
+
|
|
49
|
+
This class works as a command and monitoring server to control the Symétrie Hexapod JORAN.
|
|
50
|
+
This control server shall be used as the single point access for controlling the hardware
|
|
51
|
+
device. Monitoring access should be done preferably through this control server also,
|
|
52
|
+
but can be done with a direct connection through the PunaController if needed.
|
|
53
|
+
|
|
54
|
+
The sever binds to the following ZeroMQ sockets:
|
|
55
|
+
|
|
56
|
+
* a REQ-REP socket that can be used as a command server. Any client can connect and
|
|
57
|
+
send a command to the Hexapod.
|
|
58
|
+
|
|
59
|
+
* a PUB-SUP socket that serves as a monitoring server. It will send out Hexapod status
|
|
60
|
+
information to all the connected clients every five seconds.
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self):
|
|
65
|
+
super().__init__()
|
|
66
|
+
|
|
67
|
+
self.device_protocol = JoranProtocol(self)
|
|
68
|
+
|
|
69
|
+
self.logger.debug(f"Binding ZeroMQ socket to {self.device_protocol.get_bind_address()}")
|
|
70
|
+
|
|
71
|
+
self.device_protocol.bind(self.dev_ctrl_cmd_sock)
|
|
72
|
+
|
|
73
|
+
self.poller.register(self.dev_ctrl_cmd_sock, zmq.POLLIN)
|
|
74
|
+
|
|
75
|
+
def get_communication_protocol(self):
|
|
76
|
+
return CTRL_SETTINGS.PROTOCOL
|
|
77
|
+
|
|
78
|
+
def get_commanding_port(self):
|
|
79
|
+
return CTRL_SETTINGS.COMMANDING_PORT
|
|
80
|
+
|
|
81
|
+
def get_service_port(self):
|
|
82
|
+
return CTRL_SETTINGS.SERVICE_PORT
|
|
83
|
+
|
|
84
|
+
def get_monitoring_port(self):
|
|
85
|
+
return CTRL_SETTINGS.MONITORING_PORT
|
|
86
|
+
|
|
87
|
+
def get_storage_mnemonic(self):
|
|
88
|
+
try:
|
|
89
|
+
return CTRL_SETTINGS.STORAGE_MNEMONIC
|
|
90
|
+
except AttributeError:
|
|
91
|
+
return "JORAN"
|
|
92
|
+
|
|
93
|
+
def before_serve(self):
|
|
94
|
+
start_http_server(CTRL_SETTINGS.METRICS_PORT)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@click.group()
|
|
98
|
+
def cli():
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@cli.command()
|
|
103
|
+
@click.option("--simulator", "--sim", is_flag=True, help="Start the Hexapod Joran Simulator as the backend.")
|
|
104
|
+
def start(simulator):
|
|
105
|
+
"""Start the Hexapod Joran Control Server."""
|
|
106
|
+
|
|
107
|
+
if simulator:
|
|
108
|
+
Settings.set_simulation_mode(True)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
controller = JoranControlServer()
|
|
112
|
+
controller.serve()
|
|
113
|
+
|
|
114
|
+
except KeyboardInterrupt:
|
|
115
|
+
print("Shutdown requested...exiting")
|
|
116
|
+
|
|
117
|
+
except SystemExit as exit_code:
|
|
118
|
+
print("System Exit with code {}.".format(exit_code))
|
|
119
|
+
sys.exit(exit_code)
|
|
120
|
+
|
|
121
|
+
except Exception:
|
|
122
|
+
logger.exception("Cannot start the Hexapod Joran Control Server")
|
|
123
|
+
|
|
124
|
+
# The above line does exactly the same as the traceback, but on the logger
|
|
125
|
+
# import traceback
|
|
126
|
+
# traceback.print_exc(file=sys.stdout)
|
|
127
|
+
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@cli.command()
|
|
132
|
+
@click.option("--simulator", "--sim", is_flag=True, help="Start the Hexapod Joran Simulator as the backend.")
|
|
133
|
+
def start_bg(simulator):
|
|
134
|
+
"""Start the JORAN Control Server in the background."""
|
|
135
|
+
sim = "--simulator" if simulator else ""
|
|
136
|
+
proc = SubProcess("joran_cs", ["joran_cs", "start", sim])
|
|
137
|
+
proc.execute()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@cli.command()
|
|
141
|
+
def stop():
|
|
142
|
+
"""Send a 'quit_server' command to the Hexapod Joran Control Server."""
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
with JoranProxy() as proxy:
|
|
146
|
+
sp = proxy.get_service_proxy()
|
|
147
|
+
sp.quit_server()
|
|
148
|
+
except ConnectionError:
|
|
149
|
+
rich.print("[red]Couldn't connect to 'joran_cs', process probably not running. ")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@cli.command()
|
|
153
|
+
def status():
|
|
154
|
+
"""Request status information from the Control Server."""
|
|
155
|
+
|
|
156
|
+
protocol = CTRL_SETTINGS.PROTOCOL
|
|
157
|
+
hostname = CTRL_SETTINGS.HOSTNAME
|
|
158
|
+
port = CTRL_SETTINGS.COMMANDING_PORT
|
|
159
|
+
|
|
160
|
+
endpoint = connect_address(protocol, hostname, port)
|
|
161
|
+
|
|
162
|
+
if is_control_server_active(endpoint):
|
|
163
|
+
rich.print("JORAN Hexapod: [green]active")
|
|
164
|
+
with JoranProxy() as joran:
|
|
165
|
+
sim = joran.is_simulator()
|
|
166
|
+
connected = joran.is_connected()
|
|
167
|
+
ip = joran.get_ip_address()
|
|
168
|
+
rich.print(f"type: ALPHA+")
|
|
169
|
+
rich.print(f"mode: {'simulator' if sim else 'device'}{'' if connected else ' not'} connected")
|
|
170
|
+
rich.print(f"hostname: {ip}")
|
|
171
|
+
rich.print(f"commanding port: {port}")
|
|
172
|
+
else:
|
|
173
|
+
rich.print("JORAN Hexapod: [red]not active")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
if __name__ == "__main__":
|
|
177
|
+
logging.basicConfig(level=logging.DEBUG, format=Settings.LOG_FORMAT_FULL)
|
|
178
|
+
|
|
179
|
+
sys.exit(cli())
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from egse.command import ClientServerCommand
|
|
4
|
+
from egse.control import ControlServer
|
|
5
|
+
from egse.device import DeviceConnectionState
|
|
6
|
+
from egse.hexapod.symetrie.joran import JoranController
|
|
7
|
+
from egse.hexapod.symetrie.joran import JoranInterface
|
|
8
|
+
from egse.hexapod.symetrie.joran import JoranSimulator
|
|
9
|
+
from egse.hk import read_conversion_dict, convert_hk_names
|
|
10
|
+
from egse.metrics import define_metrics
|
|
11
|
+
from egse.protocol import CommandProtocol
|
|
12
|
+
from egse.settings import Settings
|
|
13
|
+
from egse.system import format_datetime
|
|
14
|
+
from egse.zmq_ser import bind_address
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
ctrl_settings = Settings.load("Hexapod JORAN Control Server")
|
|
19
|
+
joran_settings = Settings.load(filename="joran.yaml")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JoranCommand(ClientServerCommand):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class JoranProtocol(CommandProtocol):
|
|
27
|
+
def __init__(self, control_server: ControlServer):
|
|
28
|
+
super().__init__(control_server)
|
|
29
|
+
|
|
30
|
+
self.hk_conversion_table = read_conversion_dict(self.control_server.get_storage_mnemonic(), use_site=True)
|
|
31
|
+
|
|
32
|
+
if Settings.simulation_mode():
|
|
33
|
+
self.hexapod = JoranSimulator()
|
|
34
|
+
else:
|
|
35
|
+
self.hexapod = JoranController()
|
|
36
|
+
|
|
37
|
+
self.hexapod.connect()
|
|
38
|
+
|
|
39
|
+
self.load_commands(joran_settings.Commands, JoranCommand, JoranInterface)
|
|
40
|
+
|
|
41
|
+
self.build_device_method_lookup_table(self.hexapod)
|
|
42
|
+
|
|
43
|
+
self.metrics = define_metrics("JORAN")
|
|
44
|
+
|
|
45
|
+
def get_bind_address(self):
|
|
46
|
+
return bind_address(
|
|
47
|
+
self.control_server.get_communication_protocol(),
|
|
48
|
+
self.control_server.get_commanding_port(),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def get_device(self):
|
|
52
|
+
self.hexapod
|
|
53
|
+
|
|
54
|
+
def get_status(self):
|
|
55
|
+
status = super().get_status()
|
|
56
|
+
|
|
57
|
+
if self.state == DeviceConnectionState.DEVICE_NOT_CONNECTED and not Settings.simulation_mode():
|
|
58
|
+
return status
|
|
59
|
+
|
|
60
|
+
mach_positions = self.hexapod.get_machine_positions()
|
|
61
|
+
user_positions = self.hexapod.get_user_positions()
|
|
62
|
+
actuator_length = self.hexapod.get_actuator_length()
|
|
63
|
+
|
|
64
|
+
status.update({"mach": mach_positions, "user": user_positions, "alength": actuator_length})
|
|
65
|
+
|
|
66
|
+
return status
|
|
67
|
+
|
|
68
|
+
def get_housekeeping(self) -> dict:
|
|
69
|
+
result = dict()
|
|
70
|
+
result["timestamp"] = format_datetime()
|
|
71
|
+
|
|
72
|
+
if self.state == DeviceConnectionState.DEVICE_NOT_CONNECTED and not Settings.simulation_mode():
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
mach_positions = self.hexapod.get_machine_positions()
|
|
76
|
+
user_positions = self.hexapod.get_user_positions()
|
|
77
|
+
actuator_length = self.hexapod.get_actuator_length()
|
|
78
|
+
actuator_temperature = self.hexapod.get_temperature()
|
|
79
|
+
|
|
80
|
+
for idx, key in enumerate(["user_t_x", "user_t_y", "user_t_z", "user_r_x", "user_r_y", "user_r_z"]):
|
|
81
|
+
result[key] = user_positions[idx]
|
|
82
|
+
|
|
83
|
+
for idx, key in enumerate(["mach_t_x", "mach_t_y", "mach_t_z", "mach_r_x", "mach_r_y", "mach_r_z"]):
|
|
84
|
+
result[key] = mach_positions[idx]
|
|
85
|
+
|
|
86
|
+
for idx, key in enumerate(["alen_t_x", "alen_t_y", "alen_t_z", "alen_r_x", "alen_r_y", "alen_r_z"]):
|
|
87
|
+
result[key] = actuator_length[idx]
|
|
88
|
+
|
|
89
|
+
for idx, key in enumerate(["atemp_1", "atemp_2", "atemp_3", "atemp_4", "atemp_5", "atemp_6"]):
|
|
90
|
+
result[key] = actuator_temperature[idx]
|
|
91
|
+
|
|
92
|
+
# # TODO:
|
|
93
|
+
# # the get_general_state() method should be refactored as to return a dict instead of a
|
|
94
|
+
# # list. Also, we might want to rethink the usefulness of returning the tuple,
|
|
95
|
+
# # it the first return value ever used?
|
|
96
|
+
|
|
97
|
+
_, _ = self.hexapod.get_general_state()
|
|
98
|
+
|
|
99
|
+
result["Homing done"] = self.hexapod.is_homing_done()
|
|
100
|
+
result["In position"] = self.hexapod.is_in_position()
|
|
101
|
+
|
|
102
|
+
hk_dict = convert_hk_names(result, self.hk_conversion_table)
|
|
103
|
+
|
|
104
|
+
for key, value in hk_dict.items():
|
|
105
|
+
if key != "timestamp":
|
|
106
|
+
self.metrics[key].set(value)
|
|
107
|
+
|
|
108
|
+
return hk_dict
|
|
109
|
+
|
|
110
|
+
def is_connected(self):
|
|
111
|
+
# FIXME(rik): There must be another way to check if the socket is still alive...
|
|
112
|
+
# This will send way too many VERSION requests to the controllers.
|
|
113
|
+
# According to SO [https://stackoverflow.com/a/15175067] the best way
|
|
114
|
+
# to check for a connection drop / close is to handle the exceptions
|
|
115
|
+
# properly.... so, no polling for connections by sending it a simple
|
|
116
|
+
# command.
|
|
117
|
+
return self.hexapod.is_connected()
|