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.
- pymodaq_plugins_owis/__init__.py +13 -0
- pymodaq_plugins_owis/app/__init__.py +0 -0
- pymodaq_plugins_owis/daq_move_plugins/__init__.py +13 -0
- pymodaq_plugins_owis/daq_move_plugins/daq_move_OWIS_PS10.py +135 -0
- pymodaq_plugins_owis/daq_viewer_plugins/__init__.py +3 -0
- pymodaq_plugins_owis/daq_viewer_plugins/plugins_0D/__init__.py +13 -0
- pymodaq_plugins_owis/daq_viewer_plugins/plugins_1D/__init__.py +13 -0
- pymodaq_plugins_owis/daq_viewer_plugins/plugins_2D/__init__.py +14 -0
- pymodaq_plugins_owis/daq_viewer_plugins/plugins_ND/__init__.py +13 -0
- pymodaq_plugins_owis/exporters/__init__.py +6 -0
- pymodaq_plugins_owis/extensions/__init__.py +6 -0
- pymodaq_plugins_owis/hardware/__init__.py +0 -0
- pymodaq_plugins_owis/hardware/ps10.py +257 -0
- pymodaq_plugins_owis/models/__init__.py +6 -0
- pymodaq_plugins_owis/resources/__init__.py +0 -0
- pymodaq_plugins_owis/resources/config_template.toml +29 -0
- pymodaq_plugins_owis/scanners/__init__.py +6 -0
- pymodaq_plugins_owis/utils.py +15 -0
- pymodaq_plugins_owis-5.3.0.dist-info/METADATA +131 -0
- pymodaq_plugins_owis-5.3.0.dist-info/RECORD +23 -0
- pymodaq_plugins_owis-5.3.0.dist-info/WHEEL +4 -0
- pymodaq_plugins_owis-5.3.0.dist-info/entry_points.txt +5 -0
- pymodaq_plugins_owis-5.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
+
|
|
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,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,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.
|