pymodaq_plugins_owis 5.3.0__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,13 @@
1
+ from pathlib import Path
2
+ from .utils import Config
3
+ from pymodaq_utils.utils import get_version, PackageNotFoundError
4
+ from pymodaq_utils.logger import set_logger, get_module_name
5
+
6
+ config = Config()
7
+ try:
8
+ __version__ = get_version(__package__)
9
+ except PackageNotFoundError:
10
+ __version__ = '0.0.0dev'
11
+
12
+
13
+
File without changes
@@ -0,0 +1,13 @@
1
+ import importlib
2
+ from pathlib import Path
3
+ from .. import set_logger
4
+ logger = set_logger('move_plugins', add_to_console=False)
5
+
6
+ for path in Path(__file__).parent.iterdir():
7
+ try:
8
+ if '__init__' not in str(path):
9
+ importlib.import_module('.' + path.stem, __package__)
10
+ except Exception as e:
11
+ logger.warning("{:} plugin couldn't be loaded due to some missing packages or errors: {:}".format(path.stem, str(e)))
12
+ pass
13
+
@@ -0,0 +1,135 @@
1
+ from pymodaq.control_modules.move_utility_classes import (DAQ_Move_base, comon_parameters_fun,
2
+ main, DataActuatorType, DataActuator)
3
+ from pymodaq_utils.config import GlobalConfig
4
+ from pymodaq_utils.utils import ThreadCommand
5
+ from pymodaq_gui.parameter import Parameter
6
+ from qtpy import QtCore
7
+
8
+
9
+ from pymodaq_plugins_owis.hardware.ps10 import MockPS10Bus, PS10Axis, PS10Bus, PS10Error, slave_id_from_config
10
+
11
+ config = GlobalConfig()
12
+ AXES = config('owis', 'ps10', 'axes')
13
+
14
+
15
+ class DAQ_Move_OWIS_PS10(DAQ_Move_base):
16
+ """Actuator plugin for OWIS positioning units driven by PS 10 control units.
17
+
18
+ Each PS 10 drives one axis; several units are chained on their CANopen bus, the first one (master) being
19
+ connected by USB. One module per axis: the master module opens the bus, the others (same Controller ID, slave)
20
+ share it. The axes, their PS 10 slave ID and their mechanical data (as shown by OWISoft in
21
+ Control > Define positioning unit) are described in the [[ps10.axes]] blocks of the configuration file.
22
+
23
+ Tested with four PS 10-32 units (firmware PS10-V3.0-181010) driving HVM100N-30-HiSM, LTM60-25-HSM and
24
+ DMT65-DM6-HSM stages, on Windows 11, Python 3.12, PyMoDAQ 5.3.10. Moves, stop and the reference run (mode 4)
25
+ were tested on the hardware; the MOCK port simulates the bus for development and tests.
26
+
27
+ Installation: the USB driver of the PS 10 (supplied by OWIS with OWISoft) must be installed. The parameters of
28
+ the stages (currents, limit and reference switches, reference velocities) are those stored in the units:
29
+ configure each axis once with OWISoft. Close OWISoft before using the plugin, both use the same serial port.
30
+ """
31
+
32
+ is_multiaxes = True
33
+ _axis_names = [axis['name'] for axis in AXES]
34
+ _controller_units = [axis['units'] for axis in AXES]
35
+ _epsilons = 0.001 # mm or deg, about ten microsteps of the stages of the default configuration
36
+
37
+ data_actuator_type = DataActuatorType.DataActuator
38
+
39
+ params = [
40
+ {'title': 'Port:', 'name': 'port', 'type': 'str', 'value': config('owis', 'ps10', 'port')},
41
+ ] + comon_parameters_fun(axis_names=_axis_names)
42
+
43
+ def ini_attributes(self):
44
+ self.controller: PS10Bus | MockPS10Bus | None = None
45
+ self.axis: PS10Axis | None = None
46
+ self._home_timer = QtCore.QTimer()
47
+ self._home_timer.setInterval(100)
48
+ self._home_timer.timeout.connect(self._check_home)
49
+ self._returning_home = False
50
+
51
+ def ini_stage(self, controller=None):
52
+ """Open the bus (master module) or reuse the one of the master, then switch the chosen axis on."""
53
+ if self.is_master:
54
+ port = self.settings['port']
55
+ if port == 'MOCK':
56
+ self.controller = MockPS10Bus(slave_ids=[slave_id_from_config(axis['slave_id']) for axis in AXES])
57
+ else:
58
+ self.controller = PS10Bus(port)
59
+ else:
60
+ self.controller = controller
61
+ self._set_axis()
62
+ state = self.axis.state()
63
+ return f"{self.axis_name}: {AXES[self.axis_index_key]['stage']}, {state.name}", True
64
+
65
+ def _set_axis(self):
66
+ """Create the PS10Axis of the axis chosen in the settings and switch it on."""
67
+ self.axis = PS10Axis.from_config(self.controller, AXES[self.axis_index_key])
68
+ self.axis.init()
69
+
70
+ def close(self):
71
+ """Close the bus, only from the master module that opened it."""
72
+ if self.is_master and self.controller is not None:
73
+ self.controller.close()
74
+
75
+ def get_actuator_value(self) -> DataActuator:
76
+ """Position of the axis, read from the counter of its PS 10."""
77
+ position = DataActuator(data=self.axis.get_position(), units=self.axis_unit)
78
+ return self.get_position_with_scaling(position)
79
+
80
+ def move_abs(self, value: DataActuator):
81
+ """Start a move to the absolute target value; PyMoDAQ then polls the position."""
82
+ value = self.check_bound(value)
83
+ self.target_value = value
84
+ value = self.set_position_with_scaling(value)
85
+ self.axis.move_abs(value.value(self.axis_unit))
86
+
87
+ def move_rel(self, value: DataActuator):
88
+ """Start a move by value, as an absolute move to the current position plus value."""
89
+ value = self.check_bound(self.current_value + value) - self.current_value
90
+ self.target_value = value + self.current_value
91
+ target = self.set_position_with_scaling(self.target_value)
92
+ self.axis.move_abs(target.value(self.axis_unit))
93
+
94
+ def move_home(self):
95
+ """Reference run of the axis (REF1=4), then move to the home_position of the configuration.
96
+
97
+ PyMoDAQ does not poll the position after move_home: _check_home calls move_done at the end.
98
+ """
99
+ self._returning_home = False
100
+ self.axis.reference()
101
+ self._home_timer.start()
102
+
103
+ def _check_home(self):
104
+ """Called every 100 ms during move_home: chain the reference run and the move to home_position."""
105
+ if self.axis.is_moving():
106
+ return
107
+ if not self._returning_home:
108
+ if not self.axis.is_referenced():
109
+ self._home_timer.stop()
110
+ self.emit_status(ThreadCommand('Update_Status',
111
+ [f'reference run of {self.axis_name} failed: {self.axis.state().name}']))
112
+ self.move_done()
113
+ return
114
+ home_position = AXES[self.axis_index_key].get('home_position', 0.)
115
+ if home_position != 0.:
116
+ self._returning_home = True
117
+ self.axis.move_abs(home_position)
118
+ return
119
+ self._home_timer.stop()
120
+ self.move_done()
121
+
122
+ def stop_motion(self):
123
+ """Stop the axis and tell PyMoDAQ that the move is over."""
124
+ self._home_timer.stop()
125
+ self.axis.stop()
126
+ self.move_done()
127
+
128
+ def commit_settings(self, param: Parameter):
129
+ """Apply a change of the settings."""
130
+ if param.name() == 'axis':
131
+ self._set_axis()
132
+
133
+
134
+ if __name__ == '__main__':
135
+ main(__file__)
@@ -0,0 +1,13 @@
1
+ import importlib
2
+ from pathlib import Path
3
+ from ... import set_logger
4
+ logger = set_logger('viewer0D_plugins', add_to_console=False)
5
+
6
+ for path in Path(__file__).parent.iterdir():
7
+ try:
8
+ if '__init__' not in str(path):
9
+ importlib.import_module('.' + path.stem, __package__)
10
+ except Exception as e:
11
+ logger.warning("{:} plugin couldn't be loaded due to some missing packages or errors: {:}".format(path.stem, str(e)))
12
+ pass
13
+
@@ -0,0 +1,13 @@
1
+ import importlib
2
+ from pathlib import Path
3
+ from ... import set_logger
4
+ logger = set_logger('viewer1D_plugins', add_to_console=False)
5
+
6
+ for path in Path(__file__).parent.iterdir():
7
+ try:
8
+ if '__init__' not in str(path):
9
+ importlib.import_module('.' + path.stem, __package__)
10
+ except Exception as e:
11
+ logger.warning("{:} plugin couldn't be loaded due to some missing packages or errors: {:}".format(path.stem, str(e)))
12
+ pass
13
+
@@ -0,0 +1,14 @@
1
+ import importlib
2
+ from pathlib import Path
3
+ from ... import set_logger
4
+ logger = set_logger('viewer2D_plugins', add_to_console=False)
5
+
6
+ for path in Path(__file__).parent.iterdir():
7
+ try:
8
+ if '__init__' not in str(path):
9
+ importlib.import_module('.' + path.stem, __package__)
10
+ except Exception as e:
11
+ logger.warning("{:} plugin couldn't be loaded due to some missing packages or errors: {:}".format(path.stem, str(e)))
12
+ pass
13
+
14
+
@@ -0,0 +1,13 @@
1
+ import importlib
2
+ from pathlib import Path
3
+ from ... import set_logger
4
+ logger = set_logger('viewerND_plugins', add_to_console=False)
5
+
6
+ for path in Path(__file__).parent.iterdir():
7
+ try:
8
+ if '__init__' not in str(path):
9
+ importlib.import_module('.' + path.stem, __package__)
10
+ except Exception as e:
11
+ logger.warning("{:} plugin couldn't be loaded due to some missing packages or errors: {:}".format(path.stem, str(e)))
12
+ pass
13
+
@@ -0,0 +1,6 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created the 01/06/2023
4
+
5
+ @author: Sebastien Weber
6
+ """
@@ -0,0 +1,6 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created the 01/06/2023
4
+
5
+ @author: Sebastien Weber
6
+ """
File without changes
@@ -0,0 +1,257 @@
1
+ """Wrapper for OWIS PS 10 control units linked by a CANopen bus.
2
+
3
+ The master unit is connected by USB (virtual serial port), the slaves are reached through it by prefixing
4
+ each command with their two-digit slave ID.
5
+ """
6
+ import time
7
+ from enum import Enum
8
+
9
+ import serial
10
+
11
+ BAUDRATE = 9600
12
+ TERMINATOR = b'\r'
13
+ COMMAND_DELAY = 0.05 # s, the manual asks for 20 to 40 ms between two commands
14
+ MASTER = 'master' # slave_id of the unit connected by USB in the configuration
15
+
16
+ class PS10Error(Exception):
17
+ """Raised when a PS 10 unit does not answer a query."""
18
+
19
+
20
+ class PS10Bus:
21
+ """Serial link to the master PS 10 and, through it, to its slaves.
22
+
23
+ A unit is addressed by its slave ID: None for the master, 1, 2, 3... for the slaves.
24
+ """
25
+
26
+ def __init__(self, port: str, baudrate: int = BAUDRATE, timeout: float = 0.5):
27
+ self._serial = serial.Serial(port, baudrate, timeout=timeout)
28
+
29
+ @staticmethod
30
+ def _prefix(slave_id) -> str:
31
+ return '' if slave_id is None else f'{slave_id:02d}'
32
+
33
+ def write(self, slave_id, command: str):
34
+ """Send a command that has no reply (INIT1, PGO1...)."""
35
+ self._serial.reset_input_buffer()
36
+ self._serial.write((self._prefix(slave_id) + command).encode('ascii') + TERMINATOR)
37
+ time.sleep(COMMAND_DELAY)
38
+
39
+ def query(self, slave_id, command: str) -> str:
40
+ """Send a query (?CNT1, ?ASTAT...) and return its reply without the terminator."""
41
+ self.write(slave_id, command)
42
+ reply = self._serial.read_until(TERMINATOR)
43
+ if not reply.endswith(TERMINATOR):
44
+ unit = 'master' if slave_id is None else f'slave {slave_id}'
45
+ raise PS10Error(f'no reply from the {unit} to {command}')
46
+ return reply[:-len(TERMINATOR)].decode('ascii')
47
+
48
+ def close(self):
49
+ if self._serial.is_open:
50
+ self._serial.close()
51
+
52
+
53
+ class AxisState(str, Enum):
54
+ """States returned by ?ASTAT (PS 10 manual, command set)."""
55
+ NOT_INITIALIZED = 'I'
56
+ DISABLED = 'O'
57
+ READY = 'R'
58
+ POSITIONING = 'T'
59
+ VELOCITY_MODE = 'V'
60
+ REFERENCE_RUN = 'P'
61
+ RELEASING_LIMIT_SWITCH = 'F'
62
+ STOPPED_AT_LIMIT_SWITCH = 'L'
63
+ STOPPED_AT_BRAKE_SWITCH = 'B'
64
+ LIMIT_SWITCH_ERROR = 'A'
65
+ MOTION_CONTROLLER_ERROR = 'M'
66
+ TIMEOUT = 'Z'
67
+ PHASE_INITIALIZATION = 'H'
68
+ NOT_ENABLED = 'U'
69
+ MOTION_ERROR = 'E'
70
+ UNKNOWN = '?'
71
+
72
+
73
+ MOVING_STATES = (AxisState.POSITIONING, AxisState.VELOCITY_MODE,
74
+ AxisState.REFERENCE_RUN, AxisState.RELEASING_LIMIT_SWITCH)
75
+
76
+
77
+ def slave_id_from_config(slave_id) -> int | None:
78
+ """None for the master ('master' in the configuration), the slave ID otherwise."""
79
+ return None if slave_id == MASTER else int(slave_id)
80
+
81
+ def steps_per_unit(pitch: float, gear_ratio: float, fullsteps_per_revolution: int, microsteps: int) -> float:
82
+ """Microsteps per mm (or per degree) of a stage driven by a stepper motor.
83
+
84
+ The values are those shown by OWISoft in Control > Define positioning unit.
85
+ """
86
+ return gear_ratio * fullsteps_per_revolution * microsteps / pitch
87
+
88
+
89
+ class PS10Axis:
90
+ """The single axis of one PS 10 unit, in physical units (mm or deg).
91
+
92
+ Parameters
93
+ ----------
94
+ bus: PS10Bus
95
+ the serial link shared by all the units
96
+ slave_id: int or None
97
+ None for the master, the slave ID otherwise
98
+ steps_per_unit: float
99
+ microsteps per mm (linear stage) or per degree (rotation stage)
100
+ """
101
+ def __init__(self, bus: PS10Bus, slave_id, steps_per_unit: float):
102
+ self._bus = bus
103
+ self.slave_id = slave_id
104
+ self.steps_per_unit = steps_per_unit
105
+
106
+ @classmethod
107
+ def from_config(cls, bus: PS10Bus, axis: dict) -> 'PS10Axis':
108
+ """Build an axis from one [[ps10.axes]] block of the configuration."""
109
+ slave_id = slave_id_from_config(axis['slave_id'])
110
+ return cls(bus, slave_id, steps_per_unit(axis['pitch'], axis['gear_ratio'],
111
+ axis['fullsteps_per_revolution'], axis['microsteps']))
112
+
113
+ def init(self):
114
+ """Switch the axis on if needed and use absolute targets.
115
+
116
+ INIT1 is sent only when the axis is not ready: on a PS 10 it also clears the validity of the
117
+ reference run (?REFST1), even on an axis that is already switched on.
118
+ """
119
+ if self.state() != AxisState.READY:
120
+ self._bus.write(self.slave_id, 'INIT1')
121
+ self._bus.write(self.slave_id, 'ABSOL1')
122
+
123
+ def state(self) -> AxisState:
124
+ """Current state of the axis, see AxisState."""
125
+ reply = self._bus.query(self.slave_id, '?ASTAT')
126
+ try:
127
+ return AxisState(reply)
128
+ except ValueError:
129
+ raise PS10Error(f'unknown axis state {reply!r}') from None
130
+
131
+ def is_moving(self) -> bool:
132
+ return self.state() in MOVING_STATES
133
+
134
+ def get_position(self) -> float:
135
+ return int(self._bus.query(self.slave_id, '?CNT1')) / self.steps_per_unit
136
+
137
+ def move_abs(self, position: float):
138
+ """Start a move to position; return without waiting for the end of the move."""
139
+ steps = round(position * self.steps_per_unit)
140
+ self._bus.write(self.slave_id, f'PSET1={steps}')
141
+ self._bus.write(self.slave_id, 'PGO1')
142
+
143
+ def stop(self):
144
+ self._bus.write(self.slave_id, 'STOP1')
145
+
146
+ def reference(self, mode: int = 4):
147
+ """Start a reference run, REF1=mode in the PS 10 manual.
148
+
149
+ Mode 4, used by OWISoft for these stages, approaches the reference switch, releases it and sets the
150
+ position to 0. The run ends when the axis is no longer moving.
151
+ """
152
+ self._bus.write(self.slave_id, f'REF1={mode}')
153
+
154
+ def is_referenced(self) -> bool:
155
+ """True after a successful reference run; reset when an open-loop stepper motor is switched off."""
156
+ return self._bus.query(self.slave_id, '?REFST1') == '1'
157
+
158
+
159
+ class _SimulatedUnit:
160
+ """State of one simulated PS 10 unit."""
161
+
162
+ def __init__(self, velocity: int):
163
+ self.velocity = velocity # microsteps per second, as returned by ?PVEL1
164
+ self.state = AxisState.DISABLED
165
+ self.absolute = False
166
+ self.target = 0
167
+ self._start = 0
168
+ self._end = 0
169
+ self._start_time = 0.
170
+ self._duration = 0.
171
+ self.referenced = False
172
+
173
+ def counter(self) -> int:
174
+ """Current position in microsteps; ends the move when its duration has elapsed."""
175
+ if self.state in (AxisState.POSITIONING, AxisState.REFERENCE_RUN):
176
+ elapsed = time.perf_counter() - self._start_time
177
+ if elapsed < self._duration:
178
+ return round(self._start + (self._end - self._start) * elapsed / self._duration)
179
+ if self.state == AxisState.REFERENCE_RUN:
180
+ self.referenced = True
181
+ self.state = AxisState.READY
182
+ self._start = self._end
183
+ return self._start
184
+
185
+ def reference(self):
186
+ start = self.counter()
187
+ self._start = start
188
+ self._end = 0
189
+ self._start_time = time.perf_counter()
190
+ self._duration = abs(start) / self.velocity
191
+ self.state = AxisState.REFERENCE_RUN
192
+
193
+ def stop(self):
194
+ self._start = self.counter()
195
+ if self.state in (AxisState.POSITIONING, AxisState.REFERENCE_RUN):
196
+ self.state = AxisState.READY
197
+
198
+ def go(self):
199
+ start = self.counter()
200
+ self._start = start
201
+ self._end = self.target if self.absolute else start + self.target
202
+ self._start_time = time.perf_counter()
203
+ self._duration = abs(self._end - start) / self.velocity
204
+ self.state = AxisState.POSITIONING
205
+
206
+
207
+ class MockPS10Bus:
208
+ """Simulated PS 10 units with the same public methods as PS10Bus, used for tests and for port MOCK.
209
+
210
+ Only the commands used by PS10Axis are simulated; any other one raises PS10Error.
211
+ """
212
+
213
+ def __init__(self, slave_ids=(None,), velocity: int = 25000):
214
+ self._units = {slave_id: _SimulatedUnit(velocity) for slave_id in slave_ids}
215
+
216
+ def write(self, slave_id, command: str):
217
+ unit = self._units.get(slave_id)
218
+ if unit is None:
219
+ return # an absent unit does not react, as on the real bus
220
+ unit.counter() # update the state first
221
+ if command == 'INIT1':
222
+ unit.state = AxisState.READY
223
+ elif command == 'ABSOL1':
224
+ unit.absolute = True
225
+ elif command == 'RELAT1':
226
+ unit.absolute = False
227
+ elif command.startswith('PSET1='):
228
+ unit.target = int(command.split('=')[1])
229
+ elif command == 'PGO1':
230
+ if unit.state == AxisState.READY:
231
+ unit.go()
232
+ elif command == 'STOP1':
233
+ unit.stop()
234
+ elif command.startswith('REF1='):
235
+ if unit.state == AxisState.READY:
236
+ unit.reference()
237
+ else:
238
+ raise PS10Error(f'{command} is not simulated')
239
+
240
+ def query(self, slave_id, command: str) -> str:
241
+ unit = self._units.get(slave_id)
242
+ if unit is None:
243
+ unit_name = 'master' if slave_id is None else f'slave {slave_id}'
244
+ raise PS10Error(f'no reply from the {unit_name} to {command}')
245
+ position = unit.counter()
246
+ replies = {'?ASTAT': unit.state.value,
247
+ '?CNT1': str(position),
248
+ '?MODE1': 'ABSOL' if unit.absolute else 'RELAT',
249
+ '?PVEL1': str(unit.velocity),
250
+ '?VERSION': 'PS10-MOCK',
251
+ '?REFST1': '1' if unit.referenced else '0'}
252
+ if command not in replies:
253
+ raise PS10Error(f'{command} is not simulated')
254
+ return replies[command]
255
+
256
+ def close(self):
257
+ pass
@@ -0,0 +1,6 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Models of the extensions (PID, optimizers, DataMixer, Sequencer...) provided by this plugin package, one model per
4
+ module. Empty on purpose: see the section "Models of the extensions" of AGENTS.md and set models = true in the
5
+ [features] table of pyproject.toml once you add one.
6
+ """
File without changes
@@ -0,0 +1,29 @@
1
+ title = 'Configuration file of pymodaq_plugins_owis'
2
+
3
+ [ps10]
4
+ port = 'MOCK' # serial port of the master PS 10 (e.g. 'COM8'), MOCK for a simulated bus
5
+
6
+ # One [[ps10.axes]] block per PS 10 unit, with the values shown by OWISoft in
7
+ # Control > Define positioning unit. slave_id is 'master' for the unit connected by USB,
8
+ # else its slave ID on the CANopen bus (Control > CANopen bus > Check hardware).
9
+
10
+ [[ps10.axes]]
11
+ name = 'Z'
12
+ stage = 'HVM100N-30-HiSM'
13
+ slave_id = 'master'
14
+ units = 'mm'
15
+ pitch = 0.5 # units per revolution of the spindle (mm, or deg for a rotation stage)
16
+ gear_ratio = 1.0
17
+ fullsteps_per_revolution = 200
18
+ microsteps = 50
19
+ home_position = 15 # position reached after the reference run, in the unit of the axis
20
+
21
+ [[ps10.axes]]
22
+ name = "Theta"
23
+ stage = "DMT65-DM6-HSM"
24
+ slave_id = 2
25
+ units = "degree"
26
+ pitch = 360.0
27
+ gear_ratio = 180.0
28
+ fullsteps_per_revolution = 200
29
+ microsteps = 50
@@ -0,0 +1,6 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created the 01/06/2023
4
+
5
+ @author: Sebastien Weber
6
+ """
@@ -0,0 +1,15 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created the 31/08/2023
4
+
5
+ @author: Sebastien Weber
6
+ """
7
+ from pathlib import Path
8
+
9
+ from pymodaq_utils.config import BaseConfig, USER
10
+
11
+
12
+ class Config(BaseConfig):
13
+ """Main class to deal with configuration values for this plugin"""
14
+ config_template_path = Path(__file__).parent.joinpath('resources/config_template.toml')
15
+ config_name = f"config_{__package__.split('pymodaq_plugins_')[1]}"
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.5
2
+ Name: pymodaq_plugins_owis
3
+ Version: 5.3.0
4
+ Summary: PyMoDAQ plugin for OWIS positioning units driven by PS 10 control units
5
+ Project-URL: Homepage, https://pymodaq.cnrs.fr
6
+ Project-URL: Documentation , https://pymodaq.cnrs.fr
7
+ Project-URL: Repository , https://github.com/PyMoDAQ/pymodaq_plugins_owis
8
+ Author-email: Jérémie Margueritat <jeremie.margueritat@univ-lyon1.fr>
9
+ Maintainer-email: Jérémie Margueritat <jeremie.margueritat@univ-lyon1.fr>
10
+ License: The MIT License (MIT)
11
+
12
+ Copyright (c) 2021 Sebastien Weber <sebastien.weber@cemes.fr>
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in
22
+ all copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30
+ THE SOFTWARE.
31
+ License-File: LICENSE
32
+ Classifier: Development Status :: 5 - Production/Stable
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Natural Language :: English
36
+ Classifier: Operating System :: OS Independent
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Topic :: Scientific/Engineering :: Human Machine Interfaces
42
+ Classifier: Topic :: Scientific/Engineering :: Visualization
43
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
44
+ Classifier: Topic :: Software Development :: User Interfaces
45
+ Requires-Python: >=3.10
46
+ Requires-Dist: pymodaq-data>=5.3.0
47
+ Requires-Dist: pymodaq-gui>=5.3.0
48
+ Requires-Dist: pymodaq-utils>=5.3.1
49
+ Requires-Dist: pymodaq>=5.3.7
50
+ Requires-Dist: pyserial
51
+ Description-Content-Type: text/x-rst
52
+
53
+ pymodaq_plugins_owis
54
+ ####################
55
+
56
+ .. image:: https://img.shields.io/pypi/v/pymodaq_plugins_owis.svg
57
+ :target: https://pypi.org/project/pymodaq_plugins_owis/
58
+ :alt: Latest Version
59
+
60
+ .. image:: https://readthedocs.org/projects/pymodaq/badge/?version=latest
61
+ :target: https://pymodaq.readthedocs.io/en/stable/?badge=latest
62
+ :alt: Documentation Status
63
+
64
+ .. image:: https://github.com/PyMoDAQ/pymodaq_plugins_owis/actions/workflows/Test.yml/badge.svg
65
+ :target: https://github.com/PyMoDAQ/pymodaq_plugins_owis/actions/workflows/Test.yml
66
+
67
+
68
+ PyMoDAQ plugin for OWIS positioning units driven by PS 10 control units, chained on their CANopen bus.
69
+
70
+ This branch targets **PyMoDAQ 5.3.x** (Python 3.10 to 3.13).
71
+
72
+ Authors
73
+ =======
74
+
75
+ * Jérémie Margueritat (jeremie.margueritat@univ-lyon1.fr)
76
+
77
+ Instruments
78
+ ===========
79
+
80
+ Below is the list of instruments included in this plugin
81
+
82
+ Actuators
83
+ +++++++++
84
+
85
+ * **OWIS_PS10**: OWIS stages driven by PS 10 control units, one axis per unit, the units being chained on their
86
+ CANopen bus (tested with HVM100N-30-HiSM, LTM60-25-HSM and DMT65-DM6-HSM stages)
87
+
88
+
89
+ Installation instructions
90
+ =========================
91
+
92
+ * Tested with PyMoDAQ 5.3.10, Python 3.12, Windows 11.
93
+ * Install the USB driver of the PS 10, supplied by OWIS with OWISoft.
94
+ * Configure each stage once with OWISoft (currents, limit and reference switches, reference velocities): the plugin
95
+ uses the parameters stored in the units. Close OWISoft before using the plugin, both use the same serial port.
96
+
97
+
98
+ Configuration
99
+ =============
100
+
101
+ The serial port and the axes are read from the configuration of the plugin. Describe your setup in your local
102
+ configuration file (``config_owis.toml`` in the ``config`` folder of your PyMoDAQ user directory), with
103
+ PyMoDAQ closed, since PyMoDAQ rewrites this file when it closes. Each ``[[ps10.axes]]`` block describes one unit,
104
+ with the values shown by OWISoft in *Control > Define positioning unit*::
105
+
106
+ [ps10]
107
+ port = 'COM8' # MOCK to simulate the units
108
+
109
+ [[ps10.axes]]
110
+ name = 'X'
111
+ stage = 'LTM60-25-HSM'
112
+ slave_id = 'master' # else the slave ID of the unit on the CANopen bus
113
+ units = 'mm' # 'degree' for a rotation stage
114
+ pitch = 1.0
115
+ gear_ratio = 1.0
116
+ fullsteps_per_revolution = 200
117
+ microsteps = 50
118
+ home_position = 12.5 # position reached after the reference run
119
+
120
+ The axes of the local file replace those of the template. *Home* runs the reference run of the PS 10 (mode 4:
121
+ approach the reference switch, release it, set the position to 0), then moves the axis to its ``home_position``.
122
+
123
+
124
+ Development
125
+ ===========
126
+
127
+ * Install your plugin in editable mode: ``pip install -e .``
128
+ * Check it, without any hardware: ``check_plugin`` and ``pytest tests``
129
+ * If you use an AI coding assistant, point it to ``AGENTS.md``: it describes the patterns to follow for instrument
130
+ plugins, Dashboard extensions (``CustomExt``, whenever instruments are involved) and standalone apps
131
+ (``CustomApp``, only if no instruments are needed).
@@ -0,0 +1,23 @@
1
+ pymodaq_plugins_owis/__init__.py,sha256=kHISHcIGvr98_YfphdBAs1wThG_WCeoh_Vt0mftOxck,306
2
+ pymodaq_plugins_owis/utils.py,sha256=saBKehqiq5myeh7GCW1vYf5wq4QVYhSVLTxFK6KkdP4,419
3
+ pymodaq_plugins_owis/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pymodaq_plugins_owis/daq_move_plugins/__init__.py,sha256=H-DnuGG6lezGyUjuhavlXRbzoQqzMbZaT3mfMNnAhHk,454
5
+ pymodaq_plugins_owis/daq_move_plugins/daq_move_OWIS_PS10.py,sha256=5cRc2qG7t60r3-VHWSU7PuGDplBSCzChSoWxizoUopc,6113
6
+ pymodaq_plugins_owis/daq_viewer_plugins/__init__.py,sha256=ajz1GSNU9xYVrFEDSz6Xwg7amWQ_yvW75tQa1ZvRIWc,3
7
+ pymodaq_plugins_owis/daq_viewer_plugins/plugins_0D/__init__.py,sha256=ooWW3pJyqQ8lk_bgXe8RXvCZLzl41Cm5gJxwSc-vaCc,459
8
+ pymodaq_plugins_owis/daq_viewer_plugins/plugins_1D/__init__.py,sha256=R2Z_g54zfMJ16Kj3dA7MRj3TF8rjK8lWVAqwJ_yPYTA,459
9
+ pymodaq_plugins_owis/daq_viewer_plugins/plugins_2D/__init__.py,sha256=U43pcyA6aNdltDFbW0TbxpQ2MWYJy3BW-dNucGTjoBc,460
10
+ pymodaq_plugins_owis/daq_viewer_plugins/plugins_ND/__init__.py,sha256=AkMkmRCEIkEN8Jxnd0xFhmaTfLm-Sg95TozVstPsHLI,459
11
+ pymodaq_plugins_owis/exporters/__init__.py,sha256=vqgY3SHF6EhPcfeS8lzb7QvAiTIXWmyObIaCYeJNb6Y,81
12
+ pymodaq_plugins_owis/extensions/__init__.py,sha256=vqgY3SHF6EhPcfeS8lzb7QvAiTIXWmyObIaCYeJNb6Y,81
13
+ pymodaq_plugins_owis/hardware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ pymodaq_plugins_owis/hardware/ps10.py,sha256=vF_sppouoOUt89hX50QA7zmM_KUcAme3uJKLDwqzxzE,9620
15
+ pymodaq_plugins_owis/models/__init__.py,sha256=2rW2yi-kdYDhhu1FlnPon1lDotzVh2-IoHDR9KKYU54,311
16
+ pymodaq_plugins_owis/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ pymodaq_plugins_owis/resources/config_template.toml,sha256=5T_fw3WCOl0WExf9t5KNkDVBJYDO4RGOlUu33FcfwXM,885
18
+ pymodaq_plugins_owis/scanners/__init__.py,sha256=vqgY3SHF6EhPcfeS8lzb7QvAiTIXWmyObIaCYeJNb6Y,81
19
+ pymodaq_plugins_owis-5.3.0.dist-info/METADATA,sha256=7LUnc0Bg9P-4duQSsvC06otD8QfovNrQRRNzMFsim5s,5702
20
+ pymodaq_plugins_owis-5.3.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
21
+ pymodaq_plugins_owis-5.3.0.dist-info/entry_points.txt,sha256=waouxVib5DczcpNmEvWbLTZEqjluA8VMBUxaS2dkyPU,97
22
+ pymodaq_plugins_owis-5.3.0.dist-info/licenses/LICENSE,sha256=VKOejxexXAe3XwfhAhcFGqeXQ12irxVHdeAojZwFEI8,1108
23
+ pymodaq_plugins_owis-5.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [pymodaq.instruments]
2
+ owis = pymodaq_plugins_owis
3
+
4
+ [pymodaq.plugins]
5
+ owis = pymodaq_plugins_owis
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Sebastien Weber <sebastien.weber@cemes.fr>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.