puda-drivers 0.0.7__tar.gz → 0.0.9__tar.gz
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.
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/.gitignore +3 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/PKG-INFO +3 -2
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/pyproject.toml +3 -2
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/core/__init__.py +2 -1
- puda_drivers-0.0.9/src/puda_drivers/core/position.py +378 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/core/serialcontroller.py +23 -15
- puda_drivers-0.0.9/src/puda_drivers/cv/__init__.py +4 -0
- puda_drivers-0.0.9/src/puda_drivers/cv/camera.py +434 -0
- puda_drivers-0.0.9/src/puda_drivers/labware/__init__.py +9 -0
- puda_drivers-0.0.9/src/puda_drivers/labware/labware.py +157 -0
- puda_drivers-0.0.9/src/puda_drivers/labware/opentrons_96_tiprack_300ul.json +1020 -0
- puda_drivers-0.0.9/src/puda_drivers/labware/polyelectric_8_wellplate_30000ul.json +141 -0
- puda_drivers-0.0.9/src/puda_drivers/labware/trash_bin.json +41 -0
- puda_drivers-0.0.9/src/puda_drivers/machines/__init__.py +3 -0
- puda_drivers-0.0.9/src/puda_drivers/machines/first.py +255 -0
- puda_drivers-0.0.9/src/puda_drivers/move/__init__.py +4 -0
- puda_drivers-0.0.9/src/puda_drivers/move/deck.py +47 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/move/gcode.py +126 -117
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/transfer/liquid/sartorius/sartorius.py +31 -7
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/tests/example.py +6 -6
- puda_drivers-0.0.9/tests/first.py +64 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/tests/pipette.py +4 -4
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/tests/qubot.py +14 -11
- puda_drivers-0.0.9/tests/webcam.py +28 -0
- puda_drivers-0.0.9/uv.lock +209 -0
- puda_drivers-0.0.7/src/puda_drivers/move/__init__.py +0 -1
- puda_drivers-0.0.7/tests/together.py +0 -152
- puda_drivers-0.0.7/uv.lock +0 -28
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/LICENSE +0 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/README.md +0 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/__init__.py +0 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/core/logging.py +0 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/move/grbl/__init__.py +0 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/move/grbl/api.py +0 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/move/grbl/constants.py +0 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/py.typed +0 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/transfer/liquid/sartorius/__init__.py +0 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/transfer/liquid/sartorius/api.py +0 -0
- {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/transfer/liquid/sartorius/constants.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: puda-drivers
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.9
|
|
4
4
|
Summary: Hardware drivers for the PUDA platform.
|
|
5
5
|
Project-URL: Homepage, https://github.com/zhao-bears/puda-drivers
|
|
6
6
|
Project-URL: Issues, https://github.com/zhao-bears/puda-drivers/issues
|
|
@@ -14,7 +14,8 @@ Classifier: Programming Language :: Python :: 3
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.14
|
|
15
15
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
16
|
Classifier: Topic :: System :: Hardware
|
|
17
|
-
Requires-Python: >=3.
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Requires-Dist: opencv-python>=4.12.0.88
|
|
18
19
|
Requires-Dist: pyserial~=3.5
|
|
19
20
|
Description-Content-Type: text/markdown
|
|
20
21
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "puda-drivers"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.9"
|
|
4
4
|
description = "Hardware drivers for the PUDA platform."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
7
7
|
{ name = "zhao", email = "20024592+agentzhao@users.noreply.github.com" }
|
|
8
8
|
]
|
|
9
|
-
requires-python = ">=3.
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Intended Audience :: Science/Research",
|
|
12
12
|
"Intended Audience :: Developers",
|
|
@@ -19,6 +19,7 @@ classifiers = [
|
|
|
19
19
|
license = "MIT"
|
|
20
20
|
license-files = ["LICEN[CS]E*"]
|
|
21
21
|
dependencies = [
|
|
22
|
+
"opencv-python>=4.12.0.88",
|
|
22
23
|
"pyserial~=3.5",
|
|
23
24
|
]
|
|
24
25
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .serialcontroller import SerialController, list_serial_ports
|
|
2
2
|
from .logging import setup_logging
|
|
3
|
+
from .position import Position
|
|
3
4
|
|
|
4
|
-
__all__ = ["SerialController", "list_serial_ports", "setup_logging"]
|
|
5
|
+
__all__ = ["SerialController", "list_serial_ports", "setup_logging", "Position"]
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Position class for handling multi-axis positions.
|
|
3
|
+
|
|
4
|
+
Supports flexible axis definitions with default x, y, z, a axes.
|
|
5
|
+
Provides JSON conversion, arithmetic operations, and dictionary/tuple compatibility.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import copy
|
|
10
|
+
from typing import Dict, Optional, Tuple, Union, Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Position:
|
|
14
|
+
"""
|
|
15
|
+
Represents a position in multi-axis space.
|
|
16
|
+
|
|
17
|
+
Default axes are x, y, z, a, but can support any number of axes.
|
|
18
|
+
Supports addition, subtraction, JSON conversion, and dictionary/tuple access.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
x: Optional[float] = None,
|
|
24
|
+
y: Optional[float] = None,
|
|
25
|
+
z: Optional[float] = None,
|
|
26
|
+
a: Optional[float] = None,
|
|
27
|
+
**kwargs: float
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Initialize a Position with axis values.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
x: X axis value (optional)
|
|
34
|
+
y: Y axis value (optional)
|
|
35
|
+
z: Z axis value (optional)
|
|
36
|
+
a: A axis value (optional)
|
|
37
|
+
**kwargs: Additional axis values (e.g., b=10.0, c=20.0)
|
|
38
|
+
|
|
39
|
+
Examples:
|
|
40
|
+
>>> pos = Position(x=10, y=20, z=30, a=0)
|
|
41
|
+
>>> pos = Position(x=10, y=20, b=5.0, c=10.0)
|
|
42
|
+
"""
|
|
43
|
+
self._axes: Dict[str, float] = {}
|
|
44
|
+
|
|
45
|
+
# Set default axes if provided
|
|
46
|
+
if x is not None:
|
|
47
|
+
self._axes["x"] = float(x)
|
|
48
|
+
if y is not None:
|
|
49
|
+
self._axes["y"] = float(y)
|
|
50
|
+
if z is not None:
|
|
51
|
+
self._axes["z"] = float(z)
|
|
52
|
+
if a is not None:
|
|
53
|
+
self._axes["a"] = float(a)
|
|
54
|
+
|
|
55
|
+
# Set additional axes
|
|
56
|
+
for axis_name, value in kwargs.items():
|
|
57
|
+
self._axes[axis_name.lower()] = float(value)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_dict(cls, data: Dict[str, float], case_sensitive: bool = False) -> "Position":
|
|
61
|
+
"""
|
|
62
|
+
Create a Position from a dictionary.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
data: Dictionary with axis names as keys and values as floats
|
|
66
|
+
case_sensitive: If False, converts keys to lowercase. Defaults to False.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Position instance
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
>>> pos = Position.from_dict({"X": 10, "Y": 20, "Z": 30})
|
|
73
|
+
>>> pos = Position.from_dict({"x": 10, "y": 20, "z": 30})
|
|
74
|
+
"""
|
|
75
|
+
if case_sensitive:
|
|
76
|
+
return cls(**{k: v for k, v in data.items()})
|
|
77
|
+
else:
|
|
78
|
+
return cls(**{k.lower(): v for k, v in data.items()})
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_json(cls, json_str: str) -> "Position":
|
|
82
|
+
"""
|
|
83
|
+
Create a Position from a JSON string.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
json_str: JSON string containing axis values
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Position instance
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
>>> pos = Position.from_json('{"x": 10, "y": 20, "z": 30}')
|
|
93
|
+
"""
|
|
94
|
+
data = json.loads(json_str)
|
|
95
|
+
return cls.from_dict(data)
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_tuple(cls, values: Tuple[float, ...], axes: Optional[Tuple[str, ...]] = None) -> "Position":
|
|
99
|
+
"""
|
|
100
|
+
Create a Position from a tuple of values.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
values: Tuple of float values
|
|
104
|
+
axes: Optional tuple of axis names. Defaults to ("x", "y", "z", "a")
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Position instance
|
|
108
|
+
|
|
109
|
+
Examples:
|
|
110
|
+
>>> pos = Position.from_tuple((10, 20, 30))
|
|
111
|
+
>>> pos = Position.from_tuple((10, 20, 30, 0), ("x", "y", "z", "a"))
|
|
112
|
+
"""
|
|
113
|
+
if axes is None:
|
|
114
|
+
default_axes = ("x", "y", "z", "a")
|
|
115
|
+
axes = default_axes[:len(values)]
|
|
116
|
+
|
|
117
|
+
if len(values) != len(axes):
|
|
118
|
+
raise ValueError(f"Number of values ({len(values)}) must match number of axes ({len(axes)})")
|
|
119
|
+
|
|
120
|
+
return cls(**{axis: val for axis, val in zip(axes, values)})
|
|
121
|
+
|
|
122
|
+
def to_dict(self, uppercase: bool = False) -> Dict[str, float]:
|
|
123
|
+
"""
|
|
124
|
+
Convert Position to a dictionary.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
uppercase: If True, returns uppercase keys (X, Y, Z, A). Defaults to False.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Dictionary with axis names as keys
|
|
131
|
+
|
|
132
|
+
Examples:
|
|
133
|
+
>>> pos = Position(x=10, y=20)
|
|
134
|
+
>>> pos.to_dict() # {"x": 10, "y": 20}
|
|
135
|
+
>>> pos.to_dict(uppercase=True) # {"X": 10, "Y": 20}
|
|
136
|
+
"""
|
|
137
|
+
if uppercase:
|
|
138
|
+
return {k.upper(): v for k, v in self._axes.items()}
|
|
139
|
+
return self._axes.copy()
|
|
140
|
+
|
|
141
|
+
def to_json(self) -> str:
|
|
142
|
+
"""
|
|
143
|
+
Convert Position to a JSON string.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
JSON string representation
|
|
147
|
+
"""
|
|
148
|
+
return json.dumps(self.to_dict())
|
|
149
|
+
|
|
150
|
+
def to_tuple(self, axes: Optional[Tuple[str, ...]] = None) -> Tuple[float, ...]:
|
|
151
|
+
"""
|
|
152
|
+
Convert Position to a tuple of values.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
axes: Optional tuple of axis names to include. Defaults to all axes in order.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Tuple of float values
|
|
159
|
+
|
|
160
|
+
Examples:
|
|
161
|
+
>>> pos = Position(x=10, y=20, z=30)
|
|
162
|
+
>>> pos.to_tuple() # (10.0, 20.0, 30.0)
|
|
163
|
+
>>> pos.to_tuple(("x", "y")) # (10.0, 20.0)
|
|
164
|
+
"""
|
|
165
|
+
if axes is None:
|
|
166
|
+
axes = tuple(self._axes.keys())
|
|
167
|
+
|
|
168
|
+
return tuple(self._axes.get(axis, 0.0) for axis in axes)
|
|
169
|
+
|
|
170
|
+
def __getitem__(self, axis: str) -> float:
|
|
171
|
+
"""Get axis value by name (case-insensitive)."""
|
|
172
|
+
axis_lower = axis.lower()
|
|
173
|
+
return self._axes.get(axis_lower, 0.0)
|
|
174
|
+
|
|
175
|
+
def __setitem__(self, axis: str, value: float) -> None:
|
|
176
|
+
"""Set axis value by name (case-insensitive)."""
|
|
177
|
+
self._axes[axis.lower()] = float(value)
|
|
178
|
+
|
|
179
|
+
def __getattr__(self, name: str) -> float:
|
|
180
|
+
"""Get axis value as attribute (e.g., pos.x, pos.y)."""
|
|
181
|
+
if name.startswith("_"):
|
|
182
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
183
|
+
return self._axes.get(name.lower(), 0.0)
|
|
184
|
+
|
|
185
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
186
|
+
"""Set axis value as attribute (e.g., pos.x = 10)."""
|
|
187
|
+
if name.startswith("_") or name in dir(self):
|
|
188
|
+
super().__setattr__(name, value)
|
|
189
|
+
else:
|
|
190
|
+
if "_axes" in self.__dict__:
|
|
191
|
+
self._axes[name.lower()] = float(value)
|
|
192
|
+
else:
|
|
193
|
+
super().__setattr__(name, value)
|
|
194
|
+
|
|
195
|
+
def __add__(self, other: Union["Position", Dict[str, float], float]) -> "Position":
|
|
196
|
+
"""
|
|
197
|
+
Add two positions or add a scalar to all axes.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
other: Position, dict, or float to add
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
New Position instance
|
|
204
|
+
|
|
205
|
+
Examples:
|
|
206
|
+
>>> pos1 = Position(x=10, y=20)
|
|
207
|
+
>>> pos2 = Position(x=5, y=10)
|
|
208
|
+
>>> pos3 = pos1 + pos2 # Position(x=15, y=30)
|
|
209
|
+
>>> pos4 = pos1 + 5 # Position(x=15, y=25)
|
|
210
|
+
"""
|
|
211
|
+
if isinstance(other, Position):
|
|
212
|
+
result = Position()
|
|
213
|
+
all_axes = set(self._axes.keys()) | set(other._axes.keys())
|
|
214
|
+
for axis in all_axes:
|
|
215
|
+
result._axes[axis] = self[axis] + other[axis]
|
|
216
|
+
return result
|
|
217
|
+
elif isinstance(other, dict):
|
|
218
|
+
other_pos = Position.from_dict(other)
|
|
219
|
+
return self + other_pos
|
|
220
|
+
elif isinstance(other, (int, float)):
|
|
221
|
+
result = Position()
|
|
222
|
+
for axis, value in self._axes.items():
|
|
223
|
+
result._axes[axis] = value + other
|
|
224
|
+
return result
|
|
225
|
+
else:
|
|
226
|
+
return NotImplemented
|
|
227
|
+
|
|
228
|
+
def __radd__(self, other: Union[Dict[str, float], float]) -> "Position":
|
|
229
|
+
"""Right-side addition."""
|
|
230
|
+
return self + other
|
|
231
|
+
|
|
232
|
+
def __sub__(self, other: Union["Position", Dict[str, float], float]) -> "Position":
|
|
233
|
+
"""
|
|
234
|
+
Subtract two positions or subtract a scalar from all axes.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
other: Position, dict, or float to subtract
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
New Position instance
|
|
241
|
+
|
|
242
|
+
Examples:
|
|
243
|
+
>>> pos1 = Position(x=10, y=20)
|
|
244
|
+
>>> pos2 = Position(x=5, y=10)
|
|
245
|
+
>>> pos3 = pos1 - pos2 # Position(x=5, y=10)
|
|
246
|
+
>>> pos4 = pos1 - 5 # Position(x=5, y=15)
|
|
247
|
+
"""
|
|
248
|
+
if isinstance(other, Position):
|
|
249
|
+
result = Position()
|
|
250
|
+
all_axes = set(self._axes.keys()) | set(other._axes.keys())
|
|
251
|
+
for axis in all_axes:
|
|
252
|
+
result._axes[axis] = self[axis] - other[axis]
|
|
253
|
+
return result
|
|
254
|
+
elif isinstance(other, dict):
|
|
255
|
+
other_pos = Position.from_dict(other)
|
|
256
|
+
return self - other_pos
|
|
257
|
+
elif isinstance(other, (int, float)):
|
|
258
|
+
result = Position()
|
|
259
|
+
for axis, value in self._axes.items():
|
|
260
|
+
result._axes[axis] = value - other
|
|
261
|
+
return result
|
|
262
|
+
else:
|
|
263
|
+
return NotImplemented
|
|
264
|
+
|
|
265
|
+
def __rsub__(self, other: Union[Dict[str, float], float]) -> "Position":
|
|
266
|
+
"""Right-side subtraction."""
|
|
267
|
+
if isinstance(other, dict):
|
|
268
|
+
other_pos = Position.from_dict(other)
|
|
269
|
+
return other_pos - self
|
|
270
|
+
elif isinstance(other, (int, float)):
|
|
271
|
+
result = Position()
|
|
272
|
+
for axis, value in self._axes.items():
|
|
273
|
+
result._axes[axis] = other - value
|
|
274
|
+
return result
|
|
275
|
+
else:
|
|
276
|
+
return NotImplemented
|
|
277
|
+
|
|
278
|
+
def __mul__(self, scalar: float) -> "Position":
|
|
279
|
+
"""
|
|
280
|
+
Multiply all axes by a scalar.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
scalar: Scalar value to multiply
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
New Position instance
|
|
287
|
+
|
|
288
|
+
Examples:
|
|
289
|
+
>>> pos = Position(x=10, y=20)
|
|
290
|
+
>>> pos2 = pos * 2 # Position(x=20, y=40)
|
|
291
|
+
"""
|
|
292
|
+
if isinstance(scalar, (int, float)):
|
|
293
|
+
result = Position()
|
|
294
|
+
for axis, value in self._axes.items():
|
|
295
|
+
result._axes[axis] = value * scalar
|
|
296
|
+
return result
|
|
297
|
+
return NotImplemented
|
|
298
|
+
|
|
299
|
+
def __rmul__(self, scalar: float) -> "Position":
|
|
300
|
+
"""Right-side multiplication."""
|
|
301
|
+
return self * scalar
|
|
302
|
+
|
|
303
|
+
def __truediv__(self, scalar: float) -> "Position":
|
|
304
|
+
"""
|
|
305
|
+
Divide all axes by a scalar.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
scalar: Scalar value to divide by
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
New Position instance
|
|
312
|
+
|
|
313
|
+
Examples:
|
|
314
|
+
>>> pos = Position(x=10, y=20)
|
|
315
|
+
>>> pos2 = pos / 2 # Position(x=5, y=10)
|
|
316
|
+
"""
|
|
317
|
+
if isinstance(scalar, (int, float)):
|
|
318
|
+
if scalar == 0:
|
|
319
|
+
raise ZeroDivisionError("Cannot divide Position by zero")
|
|
320
|
+
result = Position()
|
|
321
|
+
for axis, value in self._axes.items():
|
|
322
|
+
result._axes[axis] = value / scalar
|
|
323
|
+
return result
|
|
324
|
+
return NotImplemented
|
|
325
|
+
|
|
326
|
+
def __neg__(self) -> "Position":
|
|
327
|
+
"""Negate all axes."""
|
|
328
|
+
result = Position()
|
|
329
|
+
for axis, value in self._axes.items():
|
|
330
|
+
result._axes[axis] = -value
|
|
331
|
+
return result
|
|
332
|
+
|
|
333
|
+
def __abs__(self) -> "Position":
|
|
334
|
+
"""Absolute value of all axes."""
|
|
335
|
+
result = Position()
|
|
336
|
+
for axis, value in self._axes.items():
|
|
337
|
+
result._axes[axis] = abs(value)
|
|
338
|
+
return result
|
|
339
|
+
|
|
340
|
+
def __eq__(self, other: object) -> bool:
|
|
341
|
+
"""Check equality with another Position."""
|
|
342
|
+
if not isinstance(other, Position):
|
|
343
|
+
return False
|
|
344
|
+
return self._axes == other._axes
|
|
345
|
+
|
|
346
|
+
def __repr__(self) -> str:
|
|
347
|
+
"""String representation of Position."""
|
|
348
|
+
if not self._axes:
|
|
349
|
+
return "Position()"
|
|
350
|
+
axis_strs = [f"{k}={v}" for k, v in sorted(self._axes.items())]
|
|
351
|
+
return f"Position({', '.join(axis_strs)})"
|
|
352
|
+
|
|
353
|
+
def __str__(self) -> str:
|
|
354
|
+
"""Human-readable string representation."""
|
|
355
|
+
return self.__repr__()
|
|
356
|
+
|
|
357
|
+
def get_axes(self) -> Tuple[str, ...]:
|
|
358
|
+
"""Get tuple of all axis names."""
|
|
359
|
+
return tuple(sorted(self._axes.keys()))
|
|
360
|
+
|
|
361
|
+
def has_axis(self, axis: str) -> bool:
|
|
362
|
+
"""Check if position has a specific axis."""
|
|
363
|
+
return axis.lower() in self._axes
|
|
364
|
+
|
|
365
|
+
def copy(self) -> "Position":
|
|
366
|
+
"""Create a copy of this Position."""
|
|
367
|
+
return copy.copy(self)
|
|
368
|
+
|
|
369
|
+
# swap x and y axes
|
|
370
|
+
def swap_xy(self) -> "Position":
|
|
371
|
+
"""Swap x and y axes."""
|
|
372
|
+
self._axes["x"], self._axes["y"] = self._axes["y"], self._axes["x"]
|
|
373
|
+
return self
|
|
374
|
+
|
|
375
|
+
# get x and y coordinates only
|
|
376
|
+
def get_xy(self) -> "Position":
|
|
377
|
+
"""Get x and y coordinates only."""
|
|
378
|
+
return Position(x=self._axes["x"], y=self._axes["y"])
|
|
@@ -25,14 +25,6 @@ def list_serial_ports(filter_desc: Optional[str] = None) -> List[Tuple[str, str,
|
|
|
25
25
|
|
|
26
26
|
Returns:
|
|
27
27
|
List of tuples, where each tuple contains (port_name, description, hwid).
|
|
28
|
-
|
|
29
|
-
Example:
|
|
30
|
-
>>> from puda_drivers.core import list_serial_ports
|
|
31
|
-
>>> ports = list_serial_ports()
|
|
32
|
-
>>> for port, desc, hwid in ports:
|
|
33
|
-
... print(f"{port}: {desc}")
|
|
34
|
-
>>> # Filter for specific devices
|
|
35
|
-
>>> sartorius_ports = list_serial_ports(filter_desc="Sartorius")
|
|
36
28
|
"""
|
|
37
29
|
all_ports = serial.tools.list_ports.comports()
|
|
38
30
|
filtered_ports = []
|
|
@@ -44,6 +36,9 @@ def list_serial_ports(filter_desc: Optional[str] = None) -> List[Tuple[str, str,
|
|
|
44
36
|
|
|
45
37
|
|
|
46
38
|
class SerialController(ABC):
|
|
39
|
+
"""
|
|
40
|
+
Abstract base class for serial controllers.
|
|
41
|
+
"""
|
|
47
42
|
DEFAULT_BAUDRATE = 9600
|
|
48
43
|
DEFAULT_TIMEOUT = 30 # seconds
|
|
49
44
|
POLL_INTERVAL = 0.1 # seconds
|
|
@@ -55,8 +50,6 @@ class SerialController(ABC):
|
|
|
55
50
|
self.timeout = timeout
|
|
56
51
|
self._logger = logger
|
|
57
52
|
|
|
58
|
-
self.connect()
|
|
59
|
-
|
|
60
53
|
def connect(self) -> None:
|
|
61
54
|
"""
|
|
62
55
|
Establishes the serial connection to the port.
|
|
@@ -163,7 +156,7 @@ class SerialController(ABC):
|
|
|
163
156
|
if b"ok" in response or b"err" in response:
|
|
164
157
|
break
|
|
165
158
|
|
|
166
|
-
#
|
|
159
|
+
# for sartorius since res not returning ok or err
|
|
167
160
|
if b"\xba\r" in response:
|
|
168
161
|
break
|
|
169
162
|
else:
|
|
@@ -179,11 +172,11 @@ class SerialController(ABC):
|
|
|
179
172
|
# Decode once and check the decoded string
|
|
180
173
|
decoded_response = response.decode("utf-8", errors="ignore").strip()
|
|
181
174
|
|
|
182
|
-
if "ok" in decoded_response.lower():
|
|
175
|
+
if "ok" in decoded_response.lower():
|
|
183
176
|
self._logger.debug("<- Received response: %r", decoded_response)
|
|
184
|
-
elif "err" in decoded_response.lower():
|
|
177
|
+
elif "err" in decoded_response.lower():
|
|
185
178
|
self._logger.error("<- Received error: %r", decoded_response)
|
|
186
|
-
elif "º" in decoded_response: # for sartorius
|
|
179
|
+
elif "º" in decoded_response: # for sartorius (since res not returning ok or err)
|
|
187
180
|
self._logger.debug("<- Received response: %r", decoded_response)
|
|
188
181
|
else:
|
|
189
182
|
self._logger.warning(
|
|
@@ -219,4 +212,19 @@ class SerialController(ABC):
|
|
|
219
212
|
serial.SerialTimeoutException: If no response is received within timeout
|
|
220
213
|
"""
|
|
221
214
|
self._send_command(self._build_command(command, value))
|
|
222
|
-
|
|
215
|
+
|
|
216
|
+
# Increase timeout by 60 seconds for G28 (homing) command
|
|
217
|
+
original_timeout = self.timeout
|
|
218
|
+
if "G28" in command.upper():
|
|
219
|
+
self.timeout = original_timeout + 60
|
|
220
|
+
# Also update the serial connection's timeout if connected
|
|
221
|
+
if self.is_connected and self._serial:
|
|
222
|
+
self._serial.timeout = self.timeout
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
return self._read_response()
|
|
226
|
+
finally:
|
|
227
|
+
# Restore original timeout
|
|
228
|
+
self.timeout = original_timeout
|
|
229
|
+
if self.is_connected and self._serial:
|
|
230
|
+
self._serial.timeout = original_timeout
|