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.
Files changed (39) hide show
  1. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/.gitignore +3 -0
  2. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/PKG-INFO +3 -2
  3. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/pyproject.toml +3 -2
  4. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/core/__init__.py +2 -1
  5. puda_drivers-0.0.9/src/puda_drivers/core/position.py +378 -0
  6. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/core/serialcontroller.py +23 -15
  7. puda_drivers-0.0.9/src/puda_drivers/cv/__init__.py +4 -0
  8. puda_drivers-0.0.9/src/puda_drivers/cv/camera.py +434 -0
  9. puda_drivers-0.0.9/src/puda_drivers/labware/__init__.py +9 -0
  10. puda_drivers-0.0.9/src/puda_drivers/labware/labware.py +157 -0
  11. puda_drivers-0.0.9/src/puda_drivers/labware/opentrons_96_tiprack_300ul.json +1020 -0
  12. puda_drivers-0.0.9/src/puda_drivers/labware/polyelectric_8_wellplate_30000ul.json +141 -0
  13. puda_drivers-0.0.9/src/puda_drivers/labware/trash_bin.json +41 -0
  14. puda_drivers-0.0.9/src/puda_drivers/machines/__init__.py +3 -0
  15. puda_drivers-0.0.9/src/puda_drivers/machines/first.py +255 -0
  16. puda_drivers-0.0.9/src/puda_drivers/move/__init__.py +4 -0
  17. puda_drivers-0.0.9/src/puda_drivers/move/deck.py +47 -0
  18. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/move/gcode.py +126 -117
  19. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/transfer/liquid/sartorius/sartorius.py +31 -7
  20. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/tests/example.py +6 -6
  21. puda_drivers-0.0.9/tests/first.py +64 -0
  22. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/tests/pipette.py +4 -4
  23. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/tests/qubot.py +14 -11
  24. puda_drivers-0.0.9/tests/webcam.py +28 -0
  25. puda_drivers-0.0.9/uv.lock +209 -0
  26. puda_drivers-0.0.7/src/puda_drivers/move/__init__.py +0 -1
  27. puda_drivers-0.0.7/tests/together.py +0 -152
  28. puda_drivers-0.0.7/uv.lock +0 -28
  29. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/LICENSE +0 -0
  30. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/README.md +0 -0
  31. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/__init__.py +0 -0
  32. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/core/logging.py +0 -0
  33. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/move/grbl/__init__.py +0 -0
  34. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/move/grbl/api.py +0 -0
  35. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/move/grbl/constants.py +0 -0
  36. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/py.typed +0 -0
  37. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/transfer/liquid/sartorius/__init__.py +0 -0
  38. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/transfer/liquid/sartorius/api.py +0 -0
  39. {puda_drivers-0.0.7 → puda_drivers-0.0.9}/src/puda_drivers/transfer/liquid/sartorius/constants.py +0 -0
@@ -133,3 +133,6 @@ dmypy.json
133
133
  # Log files
134
134
  logs/
135
135
  *.log
136
+
137
+ # captures
138
+ captures/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: puda-drivers
3
- Version: 0.0.7
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.14
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.7"
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.14"
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
- # Check for expected response markers for early return for sartorius
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(): # for qubot
175
+ if "ok" in decoded_response.lower():
183
176
  self._logger.debug("<- Received response: %r", decoded_response)
184
- elif "err" in decoded_response.lower(): # for qubot
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
- return self._read_response()
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
@@ -0,0 +1,4 @@
1
+ from .camera import CameraController, list_cameras
2
+
3
+ __all__ = ["CameraController", "list_cameras"]
4
+