puda-drivers 0.0.2__py3-none-any.whl → 0.0.4__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.
- puda_drivers/core/serialcontroller.py +59 -44
- puda_drivers/move/gcode.py +410 -145
- puda_drivers/transfer/liquid/sartorius/sartorius.py +289 -149
- {puda_drivers-0.0.2.dist-info → puda_drivers-0.0.4.dist-info}/METADATA +1 -3
- {puda_drivers-0.0.2.dist-info → puda_drivers-0.0.4.dist-info}/RECORD +7 -7
- {puda_drivers-0.0.2.dist-info → puda_drivers-0.0.4.dist-info}/WHEEL +0 -0
- {puda_drivers-0.0.2.dist-info → puda_drivers-0.0.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,34 +1,53 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Sartorius rLINE pipette controller.
|
|
3
|
+
|
|
4
|
+
This module provides a Python interface for controlling Sartorius rLINE® electronic
|
|
5
|
+
pipettes and robotic dispensers via serial communication.
|
|
6
|
+
|
|
7
|
+
Reference: https://api.sartorius.com/document-hub/dam/download/34901/Sartorius-rLine-technical-user-manual-v1.1.pdf
|
|
8
|
+
"""
|
|
9
|
+
|
|
2
10
|
import logging
|
|
3
11
|
from typing import Optional
|
|
12
|
+
|
|
4
13
|
from puda_drivers.core.serialcontroller import SerialController
|
|
14
|
+
|
|
5
15
|
from .constants import STATUS_CODES
|
|
6
16
|
|
|
7
17
|
|
|
8
18
|
class SartoriusDeviceError(Exception):
|
|
9
19
|
"""Custom exception raised when the Sartorius device reports an error."""
|
|
10
20
|
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
21
|
|
|
14
22
|
class SartoriusController(SerialController):
|
|
15
23
|
"""
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
Controller for Sartorius rLINE® pipettes and robotic dispensers.
|
|
25
|
+
|
|
26
|
+
This class provides methods for controlling pipette operations including
|
|
27
|
+
aspiration, dispensing, tip ejection, and speed control via serial communication.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
DEFAULT_BAUDRATE: Default baud rate for serial communication (9600)
|
|
31
|
+
DEFAULT_TIMEOUT: Default timeout for operations (10 seconds)
|
|
32
|
+
MICROLITER_PER_STEP: Conversion factor from steps to microliters (0.5 µL/step)
|
|
33
|
+
MIN_SPEED: Minimum speed setting (1)
|
|
34
|
+
MAX_SPEED: Maximum speed setting (6)
|
|
18
35
|
"""
|
|
19
36
|
|
|
20
|
-
#
|
|
37
|
+
# Protocol Constants
|
|
21
38
|
DEFAULT_BAUDRATE = 9600
|
|
22
39
|
DEFAULT_TIMEOUT = 10
|
|
23
40
|
|
|
24
41
|
PROTOCOL_SOH = "\x01"
|
|
25
42
|
SLAVE_ADDRESS = "1"
|
|
26
|
-
PROTOCOL_TERMINATOR = "º\r"
|
|
43
|
+
PROTOCOL_TERMINATOR = "º\r"
|
|
27
44
|
|
|
28
|
-
#
|
|
29
|
-
|
|
45
|
+
# Sartorius rLine Settings
|
|
46
|
+
MICROLITER_PER_STEP = 0.5
|
|
30
47
|
SUCCESS_RESPONSE = "ok"
|
|
31
48
|
ERROR_RESPONSE = "err"
|
|
49
|
+
MIN_SPEED = 1
|
|
50
|
+
MAX_SPEED = 6
|
|
32
51
|
|
|
33
52
|
def __init__(
|
|
34
53
|
self,
|
|
@@ -36,15 +55,37 @@ class SartoriusController(SerialController):
|
|
|
36
55
|
baudrate: int = DEFAULT_BAUDRATE,
|
|
37
56
|
timeout: int = DEFAULT_TIMEOUT,
|
|
38
57
|
):
|
|
58
|
+
"""
|
|
59
|
+
Initialize the Sartorius controller.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
port_name: Serial port name (e.g., '/dev/ttyUSB0' or 'COM3')
|
|
63
|
+
baudrate: Baud rate for serial communication. Defaults to 9600.
|
|
64
|
+
timeout: Timeout in seconds for operations. Defaults to 10.
|
|
65
|
+
"""
|
|
39
66
|
super().__init__(port_name, baudrate, timeout)
|
|
40
67
|
self._logger = logging.getLogger(__name__)
|
|
41
68
|
self._logger.info(
|
|
42
|
-
|
|
69
|
+
"Sartorius Controller initialized with port='%s', baudrate=%s, timeout=%s",
|
|
70
|
+
port_name,
|
|
71
|
+
baudrate,
|
|
72
|
+
timeout,
|
|
43
73
|
)
|
|
44
74
|
|
|
45
75
|
def _build_command(self, command_code: str, value: str = "") -> str:
|
|
46
|
-
"""
|
|
47
|
-
|
|
76
|
+
"""
|
|
77
|
+
Build a command string according to the Sartorius protocol.
|
|
78
|
+
|
|
79
|
+
Command format: <SOH><SLAVE_ADDRESS>R<COMMAND_CODE><VALUE><TERMINATOR>
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
command_code: Single character command code
|
|
83
|
+
value: Optional value string to append to the command
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Complete command string ready to send
|
|
87
|
+
"""
|
|
88
|
+
return (
|
|
48
89
|
self.PROTOCOL_SOH
|
|
49
90
|
+ self.SLAVE_ADDRESS
|
|
50
91
|
+ "R"
|
|
@@ -52,207 +93,306 @@ class SartoriusController(SerialController):
|
|
|
52
93
|
+ value
|
|
53
94
|
+ self.PROTOCOL_TERMINATOR
|
|
54
95
|
)
|
|
55
|
-
return cmd
|
|
56
96
|
|
|
57
|
-
def
|
|
58
|
-
"""
|
|
59
|
-
Initializes the pipette unit (RZ command).
|
|
97
|
+
def _check_response_error(self, response: str, operation: str) -> None:
|
|
60
98
|
"""
|
|
61
|
-
|
|
62
|
-
self._logger.info("** Initializing Pipette Head (RZ) **")
|
|
99
|
+
Check if a response contains an error and raise an exception if so.
|
|
63
100
|
|
|
64
|
-
|
|
65
|
-
|
|
101
|
+
Args:
|
|
102
|
+
response: Response string from the device
|
|
103
|
+
operation: Description of the operation being performed (for error message)
|
|
66
104
|
|
|
67
|
-
|
|
105
|
+
Raises:
|
|
106
|
+
SartoriusDeviceError: If the response contains an error
|
|
107
|
+
"""
|
|
108
|
+
if self.ERROR_RESPONSE in response.lower():
|
|
68
109
|
raise SartoriusDeviceError(
|
|
69
|
-
f"
|
|
110
|
+
f"{operation} failed. Device returned error: {response}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _validate_speed(self, speed: int, direction: str = "speed") -> None:
|
|
114
|
+
"""
|
|
115
|
+
Validate that a speed value is within the allowed range.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
speed: Speed value to validate
|
|
119
|
+
direction: Direction description for error message (e.g., "Inward", "Outward")
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValueError: If speed is outside the valid range
|
|
123
|
+
"""
|
|
124
|
+
if not self.MIN_SPEED <= speed <= self.MAX_SPEED:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"{direction} speed must be between {self.MIN_SPEED} and {self.MAX_SPEED}, "
|
|
127
|
+
f"got {speed}"
|
|
70
128
|
)
|
|
129
|
+
|
|
130
|
+
def _validate_no_leading_zeros(self, value: int, command_name: str) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Validate that a numeric value has no leading zeros when converted to string.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
value: Numeric value to validate
|
|
136
|
+
command_name: Command name for error message (e.g., "RP", "RE")
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
String representation of the value
|
|
140
|
+
|
|
141
|
+
Raises:
|
|
142
|
+
ValueError: If the value has leading zeros
|
|
143
|
+
"""
|
|
144
|
+
value_str = str(value)
|
|
145
|
+
if len(value_str) > 1 and value_str.startswith("0"):
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"{command_name} command value must not have leading zeros. "
|
|
148
|
+
f"Got: {value_str}"
|
|
149
|
+
)
|
|
150
|
+
return value_str
|
|
151
|
+
|
|
152
|
+
def _execute_command(
|
|
153
|
+
self, command_code: str, value: str = "", operation: str = ""
|
|
154
|
+
) -> str:
|
|
155
|
+
"""
|
|
156
|
+
Execute a command and return the response.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
command_code: Command code to execute
|
|
160
|
+
value: Optional value for the command
|
|
161
|
+
operation: Description of the operation (for logging and error messages)
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Response string from the device
|
|
165
|
+
|
|
166
|
+
Raises:
|
|
167
|
+
SartoriusDeviceError: If the device returns an error
|
|
168
|
+
"""
|
|
169
|
+
command = self._build_command(command_code, value)
|
|
170
|
+
self._send_command(command)
|
|
171
|
+
response = self._read_response()
|
|
172
|
+
self._check_response_error(response, operation or f"Command {command_code}")
|
|
173
|
+
return response
|
|
174
|
+
|
|
175
|
+
def initialize(self) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Initialize the pipette unit (RZ command).
|
|
178
|
+
|
|
179
|
+
This command resets the pipette to its initial state and should be called
|
|
180
|
+
before performing other operations.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
SartoriusDeviceError: If initialization fails
|
|
184
|
+
"""
|
|
185
|
+
self._logger.info("** Initializing Pipette Head (RZ) **")
|
|
186
|
+
self._execute_command("Z", operation="Pipette initialization")
|
|
71
187
|
self._logger.info("** Pipette Initialization Complete **")
|
|
72
188
|
|
|
73
|
-
# different from docs
|
|
74
189
|
def get_inward_speed(self) -> int:
|
|
75
190
|
"""
|
|
76
|
-
|
|
191
|
+
Query the current aspirating speed (DI command).
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Current inward speed setting (1-6)
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
SartoriusDeviceError: If the query fails
|
|
77
198
|
"""
|
|
78
|
-
command = self._build_command(command_code="DI")
|
|
79
199
|
self._logger.info("** Querying Inward Speed (DI) **")
|
|
200
|
+
response = self._execute_command("DI", operation="Inward speed query")
|
|
80
201
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
202
|
+
if len(response) < 2:
|
|
203
|
+
raise SartoriusDeviceError(
|
|
204
|
+
f"Invalid response format for inward speed query: {response}"
|
|
205
|
+
)
|
|
85
206
|
|
|
86
|
-
|
|
87
|
-
|
|
207
|
+
speed = int(response[1])
|
|
208
|
+
self._logger.info("** Current Inward Speed: %s **", speed)
|
|
209
|
+
return speed
|
|
88
210
|
|
|
89
211
|
def set_inward_speed(self, speed: int) -> None:
|
|
90
212
|
"""
|
|
91
|
-
|
|
92
|
-
"""
|
|
93
|
-
if not 1 <= speed <= 6:
|
|
94
|
-
raise ValueError("Inward speed must be between 1 and 6.")
|
|
213
|
+
Set the aspirating speed (SI command).
|
|
95
214
|
|
|
96
|
-
|
|
97
|
-
|
|
215
|
+
Args:
|
|
216
|
+
speed: Speed setting (1-6, where 1 is slowest and 6 is fastest)
|
|
98
217
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
self.
|
|
218
|
+
Raises:
|
|
219
|
+
ValueError: If speed is outside the valid range
|
|
220
|
+
SartoriusDeviceError: If setting the speed fails
|
|
221
|
+
"""
|
|
222
|
+
self._validate_speed(speed, "Inward")
|
|
223
|
+
self._logger.info("** Setting Inward Speed (SI, Speed: %s) **", speed)
|
|
224
|
+
self._execute_command("I", value=str(speed), operation="Setting inward speed")
|
|
225
|
+
self._logger.info("** Inward Speed Set to %s Successfully **", speed)
|
|
104
226
|
|
|
105
227
|
def get_outward_speed(self) -> int:
|
|
106
228
|
"""
|
|
107
|
-
|
|
229
|
+
Query the current dispensing speed (DO command).
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Current outward speed setting (1-6)
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
SartoriusDeviceError: If the query fails
|
|
108
236
|
"""
|
|
109
|
-
command = self._build_command(command_code="DO")
|
|
110
237
|
self._logger.info("** Querying Outward Speed (DO) **")
|
|
238
|
+
response = self._execute_command("DO", operation="Outward speed query")
|
|
111
239
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
240
|
+
if len(response) < 2:
|
|
241
|
+
raise SartoriusDeviceError(
|
|
242
|
+
f"Invalid response format for outward speed query: {response}"
|
|
243
|
+
)
|
|
116
244
|
|
|
117
|
-
|
|
118
|
-
|
|
245
|
+
speed = int(response[1])
|
|
246
|
+
self._logger.info("** Current Outward Speed: %s **", speed)
|
|
247
|
+
return speed
|
|
119
248
|
|
|
120
249
|
def set_outward_speed(self, speed: int) -> None:
|
|
121
250
|
"""
|
|
122
|
-
|
|
123
|
-
"""
|
|
124
|
-
if not 1 <= speed <= 6:
|
|
125
|
-
raise ValueError("Outward speed must be between 1 and 6.")
|
|
251
|
+
Set the dispensing speed (SO command).
|
|
126
252
|
|
|
127
|
-
|
|
253
|
+
Args:
|
|
254
|
+
speed: Speed setting (1-6, where 1 is slowest and 6 is fastest)
|
|
128
255
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
self._logger.info(
|
|
256
|
+
Raises:
|
|
257
|
+
ValueError: If speed is outside the valid range
|
|
258
|
+
SartoriusDeviceError: If setting the speed fails
|
|
259
|
+
"""
|
|
260
|
+
self._validate_speed(speed, "Outward")
|
|
261
|
+
self._logger.info("** Setting Outward Speed (SO, Speed: %s) **", speed)
|
|
262
|
+
self._execute_command("O", value=str(speed), operation="Setting outward speed")
|
|
263
|
+
self._logger.info("** Outward Speed Set to %s Successfully **", speed)
|
|
137
264
|
|
|
138
265
|
def run_to_position(self, position: int) -> None:
|
|
139
266
|
"""
|
|
140
|
-
|
|
141
|
-
Steps must be given without leading zeros.
|
|
142
|
-
"""
|
|
143
|
-
position_str = str(position)
|
|
144
|
-
# Check if position has leading zeros (RP030 is incorrect)
|
|
145
|
-
if len(position_str) > 1 and position_str.startswith("0"):
|
|
146
|
-
raise ValueError("Position value for RP must not have leading zeros.")
|
|
147
|
-
|
|
148
|
-
command = self._build_command(command_code="P", value=position_str)
|
|
149
|
-
self._logger.info(f"** Run to absolute Position (RP, Position: {position}) **")
|
|
150
|
-
|
|
151
|
-
self._send_command(command)
|
|
152
|
-
res: str = self._read_response()
|
|
153
|
-
if "err" in res:
|
|
154
|
-
raise SartoriusDeviceError(f"Run to position failed with error: {res}")
|
|
267
|
+
Drive the piston to an absolute step position (RP command).
|
|
155
268
|
|
|
156
|
-
|
|
269
|
+
Args:
|
|
270
|
+
position: Target position in steps (must not have leading zeros)
|
|
157
271
|
|
|
158
|
-
|
|
272
|
+
Raises:
|
|
273
|
+
ValueError: If position has leading zeros
|
|
274
|
+
SartoriusDeviceError: If the command fails
|
|
159
275
|
"""
|
|
160
|
-
|
|
276
|
+
position_str = self._validate_no_leading_zeros(position, "RP")
|
|
277
|
+
self._logger.info("** Run to absolute Position (RP, Position: %s) **", position)
|
|
278
|
+
self._execute_command("P", value=position_str, operation="Run to position")
|
|
279
|
+
self._logger.info("** Reached Position %s Successfully **", position)
|
|
280
|
+
|
|
281
|
+
def aspirate(self, amount: float) -> None:
|
|
161
282
|
"""
|
|
162
|
-
|
|
163
|
-
command = self._build_command(command_code="I", value=str(steps))
|
|
164
|
-
self._logger.info(f"** Aspirating {amount} uL (RI{steps}) **")
|
|
283
|
+
Aspirate fluid from the current location.
|
|
165
284
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if "err" in res:
|
|
169
|
-
raise SartoriusDeviceError(f"Aspirate failed with error: {res}")
|
|
170
|
-
self._logger.info(f"** Aspirated {amount} uL Successfully **")
|
|
285
|
+
Args:
|
|
286
|
+
amount: Volume to aspirate in microliters (µL)
|
|
171
287
|
|
|
172
|
-
|
|
288
|
+
Raises:
|
|
289
|
+
ValueError: If amount is negative or zero
|
|
290
|
+
SartoriusDeviceError: If aspiration fails
|
|
173
291
|
"""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"""
|
|
177
|
-
steps = amount * self.STEPS_PER_MICROLITER
|
|
292
|
+
if amount <= 0:
|
|
293
|
+
raise ValueError(f"Aspiration amount must be positive, got {amount}")
|
|
178
294
|
|
|
179
|
-
|
|
180
|
-
self._logger.info(
|
|
295
|
+
steps = int(amount / self.MICROLITER_PER_STEP)
|
|
296
|
+
self._logger.info("** Aspirating %s uL (RI%s steps) **", amount, steps)
|
|
297
|
+
self._execute_command("I", value=str(steps), operation="Aspirate")
|
|
298
|
+
self._logger.info("** Aspirated %s uL Successfully **", amount)
|
|
181
299
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
raise SartoriusDeviceError(f"Dispense failed with error: {res}")
|
|
186
|
-
self._logger.info(f"** Dispensed {amount} uL Successfully **")
|
|
300
|
+
def dispense(self, amount: float) -> None:
|
|
301
|
+
"""
|
|
302
|
+
Dispense fluid at the current location.
|
|
187
303
|
|
|
188
|
-
|
|
304
|
+
Args:
|
|
305
|
+
amount: Volume to dispense in microliters (µL)
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
ValueError: If amount is negative or zero
|
|
309
|
+
SartoriusDeviceError: If dispensing fails
|
|
189
310
|
"""
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
311
|
+
if amount <= 0:
|
|
312
|
+
raise ValueError(f"Dispense amount must be positive, got {amount}")
|
|
313
|
+
|
|
314
|
+
steps = int(amount / self.MICROLITER_PER_STEP)
|
|
315
|
+
self._logger.info("** Dispensing %s uL (RO%s steps) **", amount, steps)
|
|
316
|
+
self._execute_command("O", value=str(steps), operation="Dispense")
|
|
317
|
+
self._logger.info("** Dispensed %s uL Successfully **", amount)
|
|
318
|
+
|
|
319
|
+
def eject_tip(self, return_position: int = 30) -> None:
|
|
193
320
|
"""
|
|
194
|
-
|
|
195
|
-
position_str = str(return_position)
|
|
196
|
-
if len(position_str) > 1 and position_str.startswith("0"):
|
|
197
|
-
raise ValueError(
|
|
198
|
-
"Return position value for RE must not have leading zeros."
|
|
199
|
-
)
|
|
200
|
-
command = self._build_command(command_code="E", value=position_str)
|
|
201
|
-
self._logger.info(
|
|
202
|
-
f"** Ejecting Tip and returning to position {return_position} (RE {return_position}) **"
|
|
203
|
-
)
|
|
204
|
-
else:
|
|
205
|
-
command = self._build_command(command_code="E")
|
|
206
|
-
self._logger.info("** Ejecting Tip and returning to position 0 (RE) **")
|
|
321
|
+
Eject the pipette tip (RE command).
|
|
207
322
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
323
|
+
Args:
|
|
324
|
+
return_position: Position to return to after ejection. Defaults to 30.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
ValueError: If return_position has leading zeros
|
|
328
|
+
SartoriusDeviceError: If tip ejection fails
|
|
329
|
+
"""
|
|
330
|
+
position_str = self._validate_no_leading_zeros(return_position, "RE")
|
|
331
|
+
self._logger.info(
|
|
332
|
+
"** Ejecting Tip and returning to position %s (RE %s) **",
|
|
333
|
+
return_position,
|
|
334
|
+
return_position,
|
|
335
|
+
)
|
|
336
|
+
self._execute_command(
|
|
337
|
+
"E", value=position_str, operation="Eject tip with return position"
|
|
338
|
+
)
|
|
212
339
|
self._logger.info("** Tip Ejection Complete **")
|
|
213
340
|
|
|
214
341
|
def run_blowout(self, return_position: Optional[int] = None) -> None:
|
|
215
342
|
"""
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
343
|
+
Run the blowout cycle to clear residual liquid (RB command).
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
return_position: Optional position to return to after blowout.
|
|
347
|
+
If None, completes blowout without returning.
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
ValueError: If return_position has leading zeros
|
|
351
|
+
SartoriusDeviceError: If blowout fails
|
|
219
352
|
"""
|
|
220
353
|
if return_position is not None:
|
|
221
|
-
position_str =
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
"Return position value for RB must not have leading zeros."
|
|
225
|
-
)
|
|
226
|
-
command = self._build_command(command_code="B", value=position_str)
|
|
354
|
+
position_str = self._validate_no_leading_zeros(
|
|
355
|
+
return_position, "RB"
|
|
356
|
+
)
|
|
227
357
|
self._logger.info(
|
|
228
|
-
|
|
358
|
+
"** Running Blowout and returning to position %s (RB %s) **",
|
|
359
|
+
return_position,
|
|
360
|
+
return_position,
|
|
361
|
+
)
|
|
362
|
+
self._execute_command(
|
|
363
|
+
"B", value=position_str, operation="Blowout with return position"
|
|
229
364
|
)
|
|
230
365
|
else:
|
|
231
|
-
command = self._build_command(command_code="B")
|
|
232
366
|
self._logger.info("** Running Blowout (RB) **")
|
|
367
|
+
self._execute_command("B", operation="Blowout")
|
|
233
368
|
|
|
234
|
-
self._send_command(command)
|
|
235
|
-
res: str = self._read_response()
|
|
236
|
-
if "err" in res:
|
|
237
|
-
raise SartoriusDeviceError(f"Blowout failed with error: {res}")
|
|
238
369
|
self._logger.info("** Blowout Complete **")
|
|
239
370
|
|
|
240
371
|
def get_status(self) -> str:
|
|
241
372
|
"""
|
|
242
|
-
|
|
243
|
-
|
|
373
|
+
Query the current status of the pipette (DS command).
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Status code character (single character string)
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
SartoriusDeviceError: If the status query fails
|
|
244
380
|
"""
|
|
245
|
-
command = self._build_command(command_code="DS")
|
|
246
381
|
self._logger.info("** Querying Pipette Status (DS) **")
|
|
382
|
+
response = self._execute_command("DS", operation="Status query")
|
|
247
383
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
384
|
+
if len(response) < 2:
|
|
385
|
+
raise SartoriusDeviceError(
|
|
386
|
+
f"Invalid response format for status query: {response}"
|
|
387
|
+
)
|
|
252
388
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
389
|
+
status_code = response[1]
|
|
390
|
+
if status_code in STATUS_CODES:
|
|
391
|
+
status_message = STATUS_CODES[status_code]
|
|
392
|
+
self._logger.info("Pipette Status Code [%s]: %s", status_code, status_message)
|
|
256
393
|
else:
|
|
257
|
-
self._logger.
|
|
258
|
-
|
|
394
|
+
self._logger.warning(
|
|
395
|
+
"Pipette Status Code [%s]: Unknown Status Code", status_code
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return status_code
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: puda-drivers
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
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
|
|
@@ -158,8 +158,6 @@ for port, desc, hwid in ports:
|
|
|
158
158
|
sartorius_ports = list_serial_ports(filter_desc="Sartorius")
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
**Note:** The `list_ports()` method is also available as a static method on `SerialController` for backward compatibility, but the module-level `list_serial_ports()` function is the recommended approach.
|
|
162
|
-
|
|
163
161
|
## Requirements
|
|
164
162
|
|
|
165
163
|
- Python >= 3.14
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
puda_drivers/__init__.py,sha256=rcF5xCkMgyLlJLN3gWwJnUoW0ShPyISeyENvaqwg4Ik,503
|
|
2
2
|
puda_drivers/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
puda_drivers/core/__init__.py,sha256=JM6eWTelwcmjTGM3gprQlJWzPGEpIdRrDmbCHtGoKyM,119
|
|
4
|
-
puda_drivers/core/serialcontroller.py,sha256
|
|
4
|
+
puda_drivers/core/serialcontroller.py,sha256=c-_kMLKxz9hLhpG-54wENkLeN0bNzY1rP9zML1RV9uc,7660
|
|
5
5
|
puda_drivers/move/__init__.py,sha256=i7G5VKD5FgnmC21TLxoASVtC88IrPUTLDJrTnp99u-0,35
|
|
6
|
-
puda_drivers/move/gcode.py,sha256=
|
|
6
|
+
puda_drivers/move/gcode.py,sha256=hgI5YzvSABJBlz5QlaNoUysB_7m9dhqXXkNm-zguYqQ,21504
|
|
7
7
|
puda_drivers/move/grbl/__init__.py,sha256=vBeeti8DVN2dACi1rLmHN_UGIOdo0s-HZX6mIepLV5I,98
|
|
8
8
|
puda_drivers/move/grbl/api.py,sha256=loj8_Vap7S9qaD0ReHhgxr9Vkl6Wp7DGzyLkZyZ6v_k,16995
|
|
9
9
|
puda_drivers/move/grbl/constants.py,sha256=4736CRDzLGWVqGscLajMlrIQMyubsHfthXi4RF1CHNg,9585
|
|
10
10
|
puda_drivers/transfer/liquid/sartorius/__init__.py,sha256=QGpKz5YUwa8xCdSMXeZ0iRU-hRVqAWNPK0mlMTuzv8I,101
|
|
11
11
|
puda_drivers/transfer/liquid/sartorius/api.py,sha256=jxwIJmY2k1K2ts6NC2ZgFTe4MOiH8TGnJeqYOqNa3rE,28250
|
|
12
12
|
puda_drivers/transfer/liquid/sartorius/constants.py,sha256=mcsjLrVBH-RSodH-pszstwcEL9wwbV0vOgHbGNxZz9w,2770
|
|
13
|
-
puda_drivers/transfer/liquid/sartorius/sartorius.py,sha256=
|
|
14
|
-
puda_drivers-0.0.
|
|
15
|
-
puda_drivers-0.0.
|
|
16
|
-
puda_drivers-0.0.
|
|
17
|
-
puda_drivers-0.0.
|
|
13
|
+
puda_drivers/transfer/liquid/sartorius/sartorius.py,sha256=iW3v-YHjj4ZAfGv0x0J-XV-Y0fAAhS6xmSg2ozQm4UI,13803
|
|
14
|
+
puda_drivers-0.0.4.dist-info/METADATA,sha256=Kx3TqzraU6R_AxNW8kPRK5F9GntTiBDqk8Z7HEFDhDs,4948
|
|
15
|
+
puda_drivers-0.0.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
puda_drivers-0.0.4.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
|
|
17
|
+
puda_drivers-0.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|