one-axis-stage 0.0.2__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,20 @@
1
+ __author__ = "Lars B. Rollik"
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("one-axis-stage")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.1"
9
+
10
+
11
+ OP_MODE_LOOKUP_TO_STR = {
12
+ 0: "OP_POSITION",
13
+ 1: "OP_EXTENDED_POSITION",
14
+ 2: "OP_CURRENT_BASED_POSITION",
15
+ 3: "OP_VELOCITY",
16
+ 4: "OP_PWM",
17
+ 5: "OP_CURRENT",
18
+ }
19
+ OP_MODE_LOOKUP = {v: k for k, v in OP_MODE_LOOKUP_TO_STR.items()}
20
+ BAUDRATE_LOOKUP = {0: 9600, 1: 57600, 2: 115200, 3: 1000000}
one_axis_stage/api.py ADDED
@@ -0,0 +1,197 @@
1
+ import json
2
+ import logging
3
+ import time
4
+ from typing import Any
5
+
6
+ from one_axis_stage import BAUDRATE_LOOKUP, OP_MODE_LOOKUP, OP_MODE_LOOKUP_TO_STR
7
+ from one_axis_stage.connection import StageSerialConnection
8
+
9
+
10
+ class StageAPI(StageSerialConnection):
11
+ stage_id: int = 0
12
+
13
+ def __init__(
14
+ self, serial_port: str, baudrate: int = 115200, timeout: float = 1
15
+ ) -> None:
16
+ # init serial connection
17
+ super().__init__(serial_port=serial_port, baudrate=baudrate, timeout=timeout)
18
+
19
+ # --- GETTERS ---
20
+
21
+ def get_stage_id(self) -> int:
22
+ self.send(command="e", order="c")
23
+ info_json = self.read_line()
24
+
25
+ # to dict
26
+ stage_id_dict = json.loads(info_json)
27
+ stage_id = stage_id_dict["stage_id"]
28
+ self.stage_id = stage_id
29
+ return stage_id
30
+
31
+ def scan_for_devices(self):
32
+ self.send(command="s", order="c")
33
+ time.sleep(2)
34
+
35
+ # while returning lines, read until no more lines
36
+ scan_result = ""
37
+ while self.connection.in_waiting > 0:
38
+ line = self.read_line()
39
+ scan_result += line + "\n"
40
+ logging.debug(f"Scan line: {line}")
41
+ time.sleep(2)
42
+
43
+ return scan_result
44
+
45
+ def get_info(self, device_id: int) -> str:
46
+ """"""
47
+ self.send(command="i", data=device_id, order="!cH")
48
+ info_json = self.read_line()
49
+
50
+ # to dict
51
+ info_dict = json.loads(info_json)
52
+
53
+ # resolve baud_rate_int -> baud_rate, same for operating mode
54
+ info_dict["baud_rate"] = BAUDRATE_LOOKUP.get(info_dict["baud_rate_int"])
55
+ info_dict["operating_mode"] = self._op_mode_int_to_str(
56
+ info_dict["operating_mode_int"]
57
+ )
58
+
59
+ return info_dict
60
+
61
+ def get_info_all(self, device_ids: list[Any]) -> str:
62
+ """"""
63
+ info_all = []
64
+ for device_id in device_ids:
65
+ info_all.append(self.get_info(device_id))
66
+
67
+ # data_order = "!c" + len(device_ids) * "H"
68
+ # self.send(command="I", data=device_ids, order=data_order)
69
+ # info_json = self.read_line()
70
+ # # to dict
71
+ # info_dict = json.loads(info_json)
72
+ # return info_dict
73
+ return info_all
74
+
75
+ def get_position(self, device_id: int) -> int:
76
+ """
77
+ Get the position of a device.
78
+ """
79
+ self.send(command="p", data=device_id, order="!cH")
80
+ # read 2 bytes & combine bytes to int
81
+ position = self.read_bytes(n_bytes=2, unpack_order="!H")
82
+ if isinstance(position, tuple):
83
+ position = position[0]
84
+
85
+ logging.debug(f"Position: {position}")
86
+ return position
87
+
88
+ # --- SETTERS ---
89
+
90
+ def set_position(self, device_id: int, position: int) -> None:
91
+ """
92
+ Set the position of a device.
93
+ """
94
+ self.send(
95
+ command="m",
96
+ data=[device_id, position],
97
+ order="!cHH",
98
+ )
99
+
100
+ def set_position_multiple(self, position_tuples: list[tuple[int, int]]) -> None:
101
+ """
102
+ Set the position of multiple devices.
103
+ """
104
+ logging.debug(f"Moving to position: {position_tuples}")
105
+
106
+ data = []
107
+ data_order = "!c"
108
+ for device_id, position in position_tuples:
109
+ data.append(device_id)
110
+ data.append(position)
111
+ data_order += "HH"
112
+
113
+ logging.debug(f"Data order: {data_order}")
114
+ logging.debug(f"Position tuples: {data}")
115
+
116
+ # send
117
+ self.send(command="M", data=data, order=data_order)
118
+
119
+ def set_baudrate(
120
+ self, device_id: int, current_baudrate: int, new_baudrate: int
121
+ ) -> None:
122
+ """
123
+ Set the baudrate of a device.
124
+ """
125
+ # TODO: assert baudrate in baudrate list
126
+
127
+ self.send(
128
+ command="b",
129
+ data=[device_id, current_baudrate, new_baudrate],
130
+ order="!cHII",
131
+ )
132
+
133
+ def set_device_id(self, current_device_id: int, new_device_id: int) -> None:
134
+ """
135
+ Set the device ID of a device.
136
+ """
137
+ # TODO: assert device id range
138
+
139
+ self.send(
140
+ command="d",
141
+ data=[current_device_id, new_device_id],
142
+ order="!cHH",
143
+ )
144
+
145
+ def set_velocity(self, device_id: int, velocity: int) -> None:
146
+ """
147
+ Set the velocity of a device.
148
+ """
149
+ # TODO: confirm that this is the velocity limit
150
+ assert velocity >= 0 and velocity <= 254, f"Invalid velocity: {velocity}"
151
+
152
+ self.send(
153
+ command="v",
154
+ data=[device_id, velocity],
155
+ order="!cHH",
156
+ )
157
+
158
+ def _op_mode_str_to_int(self, op_mode: str) -> int:
159
+ """
160
+ Resolve the operating mode code from the mode string.
161
+ """
162
+ return OP_MODE_LOOKUP.get(op_mode)
163
+
164
+ def _op_mode_int_to_str(self, op_mode: int) -> str:
165
+ """
166
+ Resolve the operating mode code from the mode string.
167
+ """
168
+ return OP_MODE_LOOKUP_TO_STR.get(op_mode)
169
+
170
+ def set_operating_mode(self, device_id: int, op_mode: str | int) -> None:
171
+ """
172
+ Set the operating mode of a device.
173
+ """
174
+ # resolve mode code
175
+ assert op_mode is not None, f"Invalid operating mode: {op_mode}"
176
+
177
+ if isinstance(op_mode, str):
178
+ op_mode = self._op_mode_str_to_int(op_mode=op_mode)
179
+
180
+ self.send(
181
+ command="o",
182
+ data=[device_id, op_mode],
183
+ order="!cHH",
184
+ )
185
+
186
+ def flash(self, device_id: int, duration_ms: int, repeats: int) -> None:
187
+ """
188
+ Flash the LED of a device.
189
+ """
190
+ assert isinstance(duration_ms, int)
191
+ assert isinstance(repeats, int)
192
+
193
+ self.send(
194
+ command="f",
195
+ data=[device_id, duration_ms, repeats],
196
+ order="!cHHH",
197
+ )
one_axis_stage/axis.py ADDED
@@ -0,0 +1,123 @@
1
+ import logging
2
+
3
+ from one_axis_stage.api import StageAPI
4
+
5
+
6
+ class StageAxis:
7
+ name: str
8
+ id: int
9
+
10
+ position_raw: int = -1
11
+ position_min: int = -1
12
+ position_max: int = -1
13
+ velocity_max: int = 0
14
+ operating_mode: str = "OP_POSITION"
15
+ api: StageAPI
16
+
17
+ def __init__(
18
+ self,
19
+ name: str,
20
+ id: int,
21
+ # position_raw: int,
22
+ position_min: int,
23
+ position_max: int,
24
+ velocity_max: int,
25
+ operating_mode: str,
26
+ api: StageAPI,
27
+ **kwargs,
28
+ ) -> None:
29
+ self.name = name
30
+ self.id = id
31
+ # self.position_raw = position_raw
32
+ self.position_min = position_min
33
+ self.position_max = position_max
34
+ self.velocity_max = velocity_max
35
+ self.operating_mode = operating_mode
36
+ self.api = api
37
+
38
+ # set device mode
39
+ # self.set_operating_mode(device_id=self.id, op_mode=self.operating_mode)
40
+ # self.set_velocity(device_id=self.id, velocity=self.velocity_max)
41
+
42
+ # get attrs
43
+ # self.
44
+
45
+ def __repr__(self) -> str:
46
+ return f"StageAxis({self.name})"
47
+
48
+ def __str__(self) -> str:
49
+ return f"StageAxis: {self.name}"
50
+
51
+ def to_dict(self) -> dict:
52
+ return {
53
+ "name": self.name,
54
+ "id": self.id,
55
+ "position_raw": self.position_raw,
56
+ "position_min": self.position_min,
57
+ "position_max": self.position_max,
58
+ "velocity_max": self.velocity_max,
59
+ "operating_mode": self.operating_mode,
60
+ }
61
+
62
+ def get_info(self):
63
+ """
64
+ Get the information of the device.
65
+
66
+ Returns
67
+ -------
68
+ dict
69
+ Information of the device.
70
+
71
+ """
72
+ info = self.api.get_info(device_id=self.id)
73
+
74
+ # update Axis attributes
75
+ self.position_raw = info["position_raw"]
76
+ # self.position_deg = info["position_deg"]
77
+ self.velocity_max = info["velocity_max"]
78
+ self.operating_mode = info["operating_mode"]
79
+
80
+ return info
81
+
82
+ def get_position(self):
83
+ """
84
+ Get the position of the device.
85
+ """
86
+ self.position_raw = self.api.get_position(device_id=self.id)
87
+ logging.debug(f"Get position: {self.position_raw}")
88
+ return self.position_raw
89
+
90
+ def set_position(self, position: int) -> None:
91
+ """
92
+ Set the position of the device.
93
+ """
94
+ assert position >= self.position_min and position <= self.position_max, (
95
+ f"Invalid position: {position}"
96
+ )
97
+
98
+ self.api.set_position(device_id=self.id, position=position)
99
+ self.position_raw = position
100
+ logging.debug(f"Set position: {position}")
101
+
102
+ def set_velocity(self, velocity: int) -> None:
103
+ """
104
+ Set the velocity of the device.
105
+ """
106
+ assert velocity >= 0 and velocity <= self.velocity_max, (
107
+ f"Invalid velocity: {velocity}"
108
+ )
109
+
110
+ self.api.set_velocity(device_id=self.id, velocity=velocity)
111
+ logging.debug(f"Set velocity: {velocity}")
112
+
113
+ def set_operating_mode(self, op_mode: str | int) -> None:
114
+ """
115
+ Set the operating mode of the device.
116
+ """
117
+ assert op_mode is not None, f"Invalid operating mode: {op_mode}"
118
+
119
+ if isinstance(op_mode, str):
120
+ self._op_mode_str_to_int(op_mode=op_mode) # type: ignore[attr-defined]
121
+
122
+ self.api.set_operating_mode(device_id=self.id, op_mode=op_mode)
123
+ logging.debug(f"Set operating mode: {op_mode}")
@@ -0,0 +1,165 @@
1
+ import logging
2
+ import struct
3
+ from typing import Any
4
+
5
+ from serial import Serial
6
+
7
+
8
+ class StageSerialConnection:
9
+ serial_port: str | None = None
10
+ baudrate: int | None = None
11
+ timeout: float = 1
12
+ connection: Serial | None = None
13
+
14
+ def __init__(
15
+ self,
16
+ serial_port: str | None = None,
17
+ baudrate: int | None = None,
18
+ timeout: float | None = None,
19
+ **kwargs: Any,
20
+ ) -> None:
21
+ self.serial_port = serial_port
22
+ self.baudrate = baudrate or 115200
23
+ self.timeout = timeout or 0.1
24
+
25
+ def dict(self) -> dict:
26
+ class_data = {
27
+ "serial_port": self.serial_port,
28
+ "baudrate": self.baudrate,
29
+ "timeout": self.timeout,
30
+ }
31
+ return class_data
32
+
33
+ def __repr__(self) -> str:
34
+ return (
35
+ f"SerialConnection(serial_port={self.serial_port}, "
36
+ f"baudrate={self.baudrate}, "
37
+ f"timeout={self.timeout})"
38
+ )
39
+
40
+ def __str__(self) -> str:
41
+ return (
42
+ f"SerialConnection: {self.serial_port} @ {self.baudrate} baud, "
43
+ f"timeout={self.timeout}"
44
+ )
45
+
46
+ @property
47
+ def connected(self) -> bool:
48
+ if self.connection is not None:
49
+ return self.connection.is_open
50
+ else:
51
+ return False
52
+
53
+ def connect(self) -> "StageSerialConnection":
54
+ if not self.connected:
55
+ self.connection = Serial(
56
+ port=self.serial_port,
57
+ baudrate=self.baudrate,
58
+ timeout=self.timeout,
59
+ )
60
+ # is open?
61
+ if self.connection.is_open:
62
+ logging.info(
63
+ f"Connected to {self.serial_port} at {self.baudrate} baud."
64
+ )
65
+ else:
66
+ logging.error(f"Failed to open serial port {self.serial_port}.")
67
+
68
+ # clear buffer
69
+ self.connection.read(self.connection.in_waiting)
70
+
71
+ return self
72
+
73
+ def disconnect(self) -> None:
74
+ if self.connection is not None:
75
+ self.connection.close()
76
+ self.connection = None
77
+ logging.info(f"Disconnected from {self.serial_port}.")
78
+
79
+ def _encode(self, data: Any, order: str) -> bytes:
80
+ """Encode & pack as byte struct & flank by start/stop bytes."""
81
+ # check that data is list
82
+ if not isinstance(data, list):
83
+ data = [data]
84
+
85
+ # encode str to bytes
86
+ data_encoded = [
87
+ item.encode() if isinstance(item, str) else item for item in data
88
+ ]
89
+
90
+ # pack the data
91
+ data_packed = struct.pack(order, *data_encoded)
92
+
93
+ # flank the packed data with start/stop bytes </>
94
+ message = b"<" + data_packed + b">"
95
+
96
+ logging.debug(f"Encoded message: '{str(message)}'")
97
+ return message
98
+
99
+ def _clear_buffer(self):
100
+ self.connection.read(self.connection.in_waiting)
101
+ return not self.connection.in_waiting
102
+
103
+ def send(self, command: str, data: Any = None, order: str = None) -> None:
104
+ """"""
105
+ assert isinstance(command, str)
106
+ assert isinstance(data, list | int | str | type(None))
107
+ assert isinstance(order, str)
108
+
109
+ # combine command and data
110
+ if data is not None:
111
+ if not isinstance(data, list):
112
+ data = [data]
113
+ raw_data = [command] + data
114
+ else:
115
+ raw_data = command
116
+
117
+ # encode/pack
118
+ data_to_send = self._encode(raw_data, order=order)
119
+
120
+ # send data
121
+ if self.connected:
122
+ self._clear_buffer()
123
+ self.connection.write(data_to_send)
124
+ self.connection.flush()
125
+ logging.debug(f"Sent data: {data_to_send}")
126
+
127
+ def read_bytes(
128
+ self, n_bytes: int = None, unpack_order: str = None
129
+ ) -> tuple[Any, ...]:
130
+ """
131
+ Read n_bytes from the serial port and unpack them according to the
132
+ specified unpack_order.
133
+ The unpack_order should be a format string compatible with the
134
+ struct module.
135
+
136
+ Parameters
137
+ ----------
138
+ n_bytes : int
139
+ unpack_order : str
140
+
141
+ Returns
142
+ -------
143
+ tuple
144
+ Unpacked data as a tuple of values.
145
+
146
+ """
147
+ raw_data = self.connection.read(n_bytes)
148
+
149
+ # Check if the correct amount of data was read
150
+ if len(raw_data) != n_bytes:
151
+ raise ValueError(f"Did not receive {n_bytes} bytes from serial port")
152
+
153
+ # Unpack the data as separate variables
154
+ unpacked_bytes = struct.unpack(unpack_order, raw_data)
155
+
156
+ logging.debug(f"Unpacked bytes: {unpacked_bytes}")
157
+ return unpacked_bytes
158
+
159
+ def read_line(self) -> str:
160
+ """
161
+ Read a line from the serial port and decode it to a string.
162
+ """
163
+ line = self.connection.readline().decode("utf-8").strip()
164
+ logging.debug(f"Received line: {line}")
165
+ return line
@@ -0,0 +1,181 @@
1
+ import logging
2
+ import time
3
+ from pathlib import Path
4
+
5
+ import yaml
6
+
7
+ from one_axis_stage.api import StageAPI
8
+ from one_axis_stage.axis import StageAxis
9
+
10
+
11
+ class StageController:
12
+ serial_port: str
13
+ baudrate: int
14
+ timeout: float
15
+
16
+ api: StageAPI
17
+ axes: dict = {}
18
+ known_positions: dict = {}
19
+
20
+ def __init__(
21
+ self,
22
+ serial_port: str | None = None,
23
+ baudrate: int | None = None,
24
+ timeout: float | None = None,
25
+ # **kwargs,
26
+ ) -> None:
27
+ if serial_port is not None:
28
+ self.serial_port = str(serial_port)
29
+ if baudrate is not None:
30
+ self.baudrate = int(baudrate)
31
+ if timeout is not None:
32
+ self.timeout = float(timeout)
33
+
34
+ # if serial port is provided, connect to the stage
35
+ if self.serial_port is not None:
36
+ self.connect()
37
+
38
+ # getter/setter for config dict
39
+ @property
40
+ def config(self) -> dict:
41
+ return {
42
+ "stage_id": self.api.stage_id,
43
+ "connection": {
44
+ "serial_port": self.serial_port,
45
+ "baudrate": self.baudrate,
46
+ "timeout": self.timeout,
47
+ },
48
+ "axes": {
49
+ axis_name: axis.__dict__() for axis_name, axis in self.axes.items()
50
+ },
51
+ "known_positions": self.known_positions,
52
+ }
53
+
54
+ @config.setter
55
+ def config(self, config: dict) -> None:
56
+ self.serial_port = config["connection"]["serial_port"]
57
+ self.baudrate = config["connection"]["baudrate"]
58
+ self.timeout = config["connection"]["timeout"]
59
+
60
+ self.axes = {}
61
+ for axis_name, axis_config in config["axes"].items():
62
+ self.add_axis(axis_name, **axis_config)
63
+ time.sleep(1)
64
+
65
+ # self.axes = {
66
+ # axis_name: StageAxis(controller=self, name=axis_name, **axis_config)
67
+ # for axis_name, axis_config in config["axes"].items()
68
+ # }
69
+ self.known_positions = config["known_positions"]
70
+
71
+ def connect(self) -> None:
72
+ self.api = StageAPI(
73
+ serial_port=self.serial_port,
74
+ baudrate=self.baudrate,
75
+ timeout=self.timeout,
76
+ )
77
+ self.api.connect()
78
+ self.api.get_stage_id()
79
+
80
+ # factory function to create a StageController instance from a configuration file
81
+ @staticmethod
82
+ def from_config(config_file: str | Path | dict) -> "StageController":
83
+ if isinstance(config_file, str):
84
+ config_file = Path(config_file)
85
+
86
+ if isinstance(config_file, Path):
87
+ config_file = Path(config_file)
88
+ # check file exists
89
+ if not config_file.is_file():
90
+ raise FileNotFoundError(f"Config file not found: {config_file}")
91
+
92
+ config_file.open("r")
93
+ with config_file.open("r") as file:
94
+ # yaml full load
95
+ stage_config = yaml.full_load(file)
96
+ elif isinstance(config_file, dict):
97
+ stage_config = config_file
98
+ else:
99
+ raise TypeError(f"Config type {config_file} is not supported")
100
+
101
+ ctrl = StageController(**stage_config["connection"])
102
+ ctrl.config = stage_config
103
+
104
+ # ctrl.ping_axes()
105
+
106
+ return ctrl
107
+
108
+ def save_config(self, config_file: str | Path, overwrite: bool = True) -> bool:
109
+ config_file = Path(config_file)
110
+ # check file exists
111
+ if config_file.exists() and not overwrite:
112
+ raise FileExistsError(f"Config file already exists: {config_file}")
113
+
114
+ with config_file.open("w") as file:
115
+ yaml.dump(self.config, file)
116
+
117
+ return config_file.exists()
118
+
119
+ def add_axis(self, axis_name: str, **axis_config) -> None:
120
+ if "name" in axis_config:
121
+ axis_name = axis_config.pop("name")
122
+
123
+ if axis_name in self.axes:
124
+ raise ValueError(f"Axis already exists: {axis_name}")
125
+
126
+ self.axes[axis_name] = StageAxis(api=self.api, name=axis_name, **axis_config)
127
+ info = self.axes[axis_name].get_info()
128
+ logging.debug(f"Controller: Added new axis {axis_name} ({info})")
129
+
130
+ return self.axes[axis_name]
131
+
132
+ def ping_axes(self) -> None:
133
+ for axis in self.axes.values():
134
+ axis.get_info()
135
+
136
+ def move_to_position(self, position: dict) -> None:
137
+ logging.debug(f"Controller: Move to position: {position}")
138
+
139
+ # lookup axis by name and move to position
140
+ position_by_axis_id = []
141
+ for axis_name in position:
142
+ axis_id = self.axes[axis_name].id
143
+ axis_target = position[axis_name]
144
+ logging.debug(f"Controller: Move to axis: {axis_id} -> {axis_target}")
145
+ if "position_raw" in axis_target:
146
+ axis_target_position = axis_target["position_raw"]
147
+ logging.debug(f"removing key position_raw from target {axis_target}")
148
+ else:
149
+ axis_target_position = axis_target
150
+
151
+ position_by_axis_id.append((axis_id, axis_target_position))
152
+
153
+ # position_by_axis_id = [
154
+ # (self.axes[axis_name].id, position[axis_name]["position_raw"])
155
+ # for axis_name in position
156
+ # ]
157
+ # # FIXME: should have to write position_raw here
158
+
159
+ return self.api.set_position_multiple(position_tuples=position_by_axis_id)
160
+
161
+ def move_to_known_position(self, position_name: str) -> None:
162
+ position = self.known_positions.get(position_name)
163
+
164
+ if position is None:
165
+ raise ValueError(f"Unknown position: {position_name}")
166
+
167
+ return self.move_to_position(position)
168
+
169
+ def save_as_known_position(self, position_name: str) -> None:
170
+ new_position = {}
171
+ for axis_name in self.axes:
172
+ # self.axes[axis_name].dict()
173
+ self.axes[axis_name].get_info()
174
+ axis_dict = self.axes[axis_name].__dict__()
175
+ new_position[axis_name] = {"position_raw": axis_dict["position_raw"]}
176
+
177
+ self.known_positions[position_name] = new_position
178
+ logging.debug(f"Controller: Save as known position: {new_position}")
179
+
180
+ def remove_known_position(self, position_name: str) -> None:
181
+ self.known_positions.pop(position_name)
@@ -0,0 +1,94 @@
1
+ import logging
2
+ from functools import partial
3
+
4
+ from one_axis_stage.controller import StageController
5
+
6
+
7
+ class MoveInterface:
8
+ """"""
9
+
10
+ controller: StageController
11
+ small_increment: int
12
+ large_increment: int
13
+
14
+ def __init__(
15
+ self,
16
+ controller: StageController,
17
+ small_increment: int = 20,
18
+ large_increment: int = 40,
19
+ ):
20
+ super().__init__()
21
+ self.controller = controller
22
+ self.small_increment = small_increment
23
+ self.large_increment = large_increment
24
+
25
+ for axis_name in self.controller.axes:
26
+ logging.info(f"Creating move methods for axis '{axis_name}'")
27
+ # slow
28
+ setattr(
29
+ self,
30
+ f"{axis_name}p",
31
+ partial(
32
+ self.move_axis_by_increment,
33
+ axis_name=axis_name,
34
+ direction_forward=True,
35
+ fast_mode=False,
36
+ ),
37
+ )
38
+ setattr(
39
+ self,
40
+ f"{axis_name}m",
41
+ partial(
42
+ self.move_axis_by_increment,
43
+ axis_name=axis_name,
44
+ direction_forward=False,
45
+ fast_mode=False,
46
+ ),
47
+ )
48
+ # fast
49
+ setattr(
50
+ self,
51
+ f"{axis_name}pp",
52
+ partial(
53
+ self.move_axis_by_increment,
54
+ axis_name=axis_name,
55
+ direction_forward=True,
56
+ fast_mode=True,
57
+ ),
58
+ )
59
+ setattr(
60
+ self,
61
+ f"{axis_name}mm",
62
+ partial(
63
+ self.move_axis_by_increment,
64
+ axis_name=axis_name,
65
+ direction_forward=False,
66
+ fast_mode=True,
67
+ ),
68
+ )
69
+
70
+ def move_axis_by_increment(
71
+ self,
72
+ axis_name: str,
73
+ direction_forward: bool = True,
74
+ fast_mode: bool = False,
75
+ ):
76
+ axis = self.controller.axes.get(axis_name)
77
+ if axis:
78
+ increment_speed = (
79
+ self.small_increment if not fast_mode else self.large_increment
80
+ )
81
+ increment = increment_speed if direction_forward else -increment_speed
82
+ new_position = axis.position_raw + increment
83
+ logging.info(
84
+ f"Moving axis '{axis.name}' {axis.position_raw}->{new_position} ({increment})"
85
+ )
86
+
87
+ # try to move, but ignore errors, only report
88
+ try:
89
+ axis.set_position(new_position)
90
+ except AssertionError as e:
91
+ logging.error(e)
92
+
93
+ else:
94
+ raise ValueError(f"Axis '{axis_name}' not found in controller")
File without changes
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: one-axis-stage
3
+ Version: 0.0.2
4
+ Summary: Hardware design and software for modular, low-cost one-axis stages
5
+ Project-URL: Homepage, https://github.com/murineshiftwork/one-axis-stage
6
+ Project-URL: Documentation, https://murineshiftwork.github.io/one-axis-stage/
7
+ Project-URL: Issue Tracker, https://github.com/murineshiftwork/one-axis-stage/issues
8
+ Author-email: "Lars B. Rollik" <lars@rollik.me>
9
+ License: BSD 3-Clause License
10
+
11
+ Copyright (c) 2021-present, Lars B. Rollik
12
+ All rights reserved.
13
+
14
+ Redistribution and use in source and binary forms, with or without
15
+ modification, are permitted provided that the following conditions are met:
16
+
17
+ 1. Redistributions of source code must retain the above copyright notice, this
18
+ list of conditions and the following disclaimer.
19
+
20
+ 2. Redistributions in binary form must reproduce the above copyright notice,
21
+ this list of conditions and the following disclaimer in the documentation
22
+ and/or other materials provided with the distribution.
23
+
24
+ 3. Neither the name of the copyright holder nor the names of its
25
+ contributors may be used to endorse or promote products derived from
26
+ this software without specific prior written permission.
27
+
28
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
29
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
30
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
31
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
32
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
33
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
34
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
35
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
36
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38
+ License-File: LICENSE
39
+ Requires-Python: >=3.10
40
+ Requires-Dist: pyserial
41
+ Requires-Dist: pyyaml
42
+ Provides-Extra: dev
43
+ Requires-Dist: commitizen; extra == 'dev'
44
+ Requires-Dist: mypy; extra == 'dev'
45
+ Requires-Dist: pre-commit; extra == 'dev'
46
+ Requires-Dist: pytest; extra == 'dev'
47
+ Requires-Dist: pytest-cov; extra == 'dev'
48
+ Requires-Dist: ruff; extra == 'dev'
49
+ Requires-Dist: types-pyyaml; extra == 'dev'
50
+ Provides-Extra: docs
51
+ Requires-Dist: mkdocs-material; extra == 'docs'
52
+ Description-Content-Type: text/markdown
53
+
54
+ [//]: # (Links)
55
+ [Github-flavored markdown]: https://github.github.com/gfm
56
+
57
+ [manifest]: https://packaging.python.org/en/latest/guides/using-manifest-in
58
+ [packaging]: https://packaging.python.org/en/latest/tutorials/packaging-projects
59
+ [setup.cfg]: https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
60
+
61
+ [bump2version]: (https://github.com/c4urself/bump2version
62
+ [pre-commit]: https://pre-commit.com
63
+
64
+ [//]: # ([black]: https://github.com/psf/black)
65
+ [ruff]: https://docs.astral.sh/ruff
66
+ [mypy]: https://mypy.readthedocs.io
67
+
68
+ [pypi]: pypi.org
69
+ [test.pypi]: test.pypi.org
70
+
71
+ [Zenodo]: https://zenodo.org
72
+
73
+ [contribution guidelines]: https://github.com/larsrollik/one-axis-stage/blob/main/CONTRIBUTING.md
74
+ [issues]: https://github.com/larsrollik/one-axis-stage/issues
75
+ [BSD 3-Clause License]: https://github.com/larsrollik/one-axis-stage/blob/main/LICENSE
76
+ [Github]: https://github.com/larsrollik/one-axis-stage/settings/secrets/actions/new
77
+ [release]: https://github.com/larsrollik/one-axis-stage/releases/new
78
+
79
+ [//]: # (Badges)
80
+
81
+ [![Contributions](https://img.shields.io/badge/Contributions-Welcome-brightgreen.svg)](https://github.com/larsrollik/one-axis-stage/blob/main/CONTRIBUTING.md)
82
+ ![CI](https://img.shields.io/github/actions/workflow/status/larsrollik/one-axis-stage/pre-pr-checks.yaml?branch=main&label=build)
83
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
84
+ [![Website](https://img.shields.io/website?up_message=online&url=https%3A%2F%2Fgithub.com/larsrollik/one-axis-stage)](https://github.com/larsrollik/one-axis-stage)
85
+
86
+ [//]: # ([![PyPI]&#40;https://img.shields.io/pypi/v/one-axis-stage.svg&#41;]&#40;https://pypi.org/project/one-axis-stage&#41;)
87
+ [//]: # ([![Wheel]&#40;https://img.shields.io/pypi/wheel/one-axis-stage.svg&#41;]&#40;https://pypi.org/project/one-axis-stage&#41;)
88
+
89
+ # one-axis-stage
90
+ Hardware design and software for modular, low-cost one-axis stages
91
+ ---
92
+ **Version: "0.0.1"**
93
+
94
+ ```-> TODO: add image```
95
+
96
+ ## Hardware build
97
+
98
+ ### Parts list
99
+
100
+ ##### Commercially available parts
101
+
102
+ | Part name | Product code | Amount (#) | Cost per part (GBP) | Total cost (GBP) | |
103
+ |-------------------------------|-----------------------|------------|---------------------|------------------|------------------------------------------------------------------------------------------------------|
104
+ | Arduino Mega 2560 | | 1 | 20 | | https://docs.arduino.cc/hardware/mega-2560 |
105
+ | Dynamixel Arduino shield | | 1 | 20 | | https://emanual.robotis.com/docs/en/parts/interface/dynamixel_shield/ |
106
+ | Dynamixel XL-320 motor | XL-320 | 1+ | 20 | | https://emanual.robotis.com/docs/en/dxl/x/xl320/ |
107
+ | USB to TTL adapter | LN-101 or MIKROE-3063 | 1 | 14 / 13 | | https://emanual.robotis.com/docs/en/parts/interface/ln-101 / https://www.mikroe.com/usb-uart-3-click |
108
+ | USB-B to other USB (computer) | | 1 | | | |
109
+ | Linear slide 26mm range | BSP1035SL | 2+ | 26 | | https://uk.rs-online.com/web/p/linear-slides/0749301 |
110
+ | | | | | | |
111
+
112
+ ##### 3D-printed parts
113
+
114
+ | Part | Count |
115
+ |------|-------|
116
+ | | |
117
+ | | |
118
+ | | |
119
+
120
+ ### Assembly instructions
121
+
122
+ ```-> TODO: add info on how to assemble the hardware```
123
+
124
+ ## Software controller
125
+
126
+ ### Example usage
127
+
128
+ ```python
129
+ from one_axis_stage import StageController
130
+
131
+ ```
132
+
133
+
134
+ ## Contributing
135
+ Contributions are very welcome!
136
+ Please see the [contribution guidelines] or check out the [issues]
137
+
138
+
139
+ ## License
140
+ This software is released under the **[BSD 3-Clause License]**
@@ -0,0 +1,11 @@
1
+ one_axis_stage/__init__.py,sha256=Dq1imqtnE5ooaYlUokYrmDxk8ypBe-Fzp_faL8blnv8,503
2
+ one_axis_stage/api.py,sha256=l_1lYoQUgUsJo4J-1hYx8NpWmpgZ-03I-oyTzOtI4PQ,5759
3
+ one_axis_stage/axis.py,sha256=CSnU1XmeaDX-dug8XKDnQ3yfad_q_zpwC_l9gXKmW7I,3497
4
+ one_axis_stage/connection.py,sha256=ecVqqdS9zDbDXnmgAVSwU_jhH9KX_tyRYwnPiJ1cpQc,4891
5
+ one_axis_stage/controller.py,sha256=V9wRqtCTJ-T-KfhXbPJSRIefxTyjHHwBfTFdwFRnxdE,6170
6
+ one_axis_stage/interface.py,sha256=0fLf8q9-RTH9PHzF0cc9XETuRuhCEwMrojcoQxD84pM,2812
7
+ one_axis_stage/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ one_axis_stage-0.0.2.dist-info/METADATA,sha256=iFZQVwdw8Eupa_1gY_j69zwc0pwxPea4uQ5DASgBryA,7208
9
+ one_axis_stage-0.0.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
10
+ one_axis_stage-0.0.2.dist-info/licenses/LICENSE,sha256=mup8CL1NS8uJb0HhKkmUbOapCMXvL_Vf5CG2VsqTnjM,1530
11
+ one_axis_stage-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2021-present, Lars B. Rollik
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.