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.
@@ -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()