puda-drivers 0.0.5__tar.gz → 0.0.7__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.5 → puda_drivers-0.0.7}/.gitignore +4 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/PKG-INFO +56 -1
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/README.md +55 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/pyproject.toml +1 -1
- puda_drivers-0.0.7/src/puda_drivers/core/__init__.py +4 -0
- puda_drivers-0.0.7/src/puda_drivers/core/logging.py +73 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/core/serialcontroller.py +19 -6
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/move/gcode.py +24 -24
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/transfer/liquid/sartorius/sartorius.py +57 -72
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/tests/pipette.py +27 -19
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/tests/qubot.py +19 -22
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/tests/together.py +6 -12
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/uv.lock +1 -1
- puda_drivers-0.0.5/src/puda_drivers/core/__init__.py +0 -3
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/LICENSE +0 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/__init__.py +0 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/move/__init__.py +0 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/move/grbl/__init__.py +0 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/move/grbl/api.py +0 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/move/grbl/constants.py +0 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/py.typed +0 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/transfer/liquid/sartorius/__init__.py +0 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/transfer/liquid/sartorius/api.py +0 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/transfer/liquid/sartorius/constants.py +0 -0
- {puda_drivers-0.0.5 → puda_drivers-0.0.7}/tests/example.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.7
|
|
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
|
|
@@ -27,6 +27,7 @@ Hardware drivers for the PUDA (Physical Unified Device Architecture) platform. T
|
|
|
27
27
|
- **Gantry Control**: Control G-code compatible motion systems (e.g., QuBot)
|
|
28
28
|
- **Liquid Handling**: Interface with Sartorius rLINE® pipettes and dispensers
|
|
29
29
|
- **Serial Communication**: Robust serial port management with automatic reconnection
|
|
30
|
+
- **Logging**: Configurable logging with optional file output to logs folder
|
|
30
31
|
- **Cross-platform**: Works on Linux, macOS, and Windows
|
|
31
32
|
|
|
32
33
|
## Installation
|
|
@@ -47,6 +48,37 @@ pip install -e .
|
|
|
47
48
|
|
|
48
49
|
## Quick Start
|
|
49
50
|
|
|
51
|
+
### Logging Configuration
|
|
52
|
+
|
|
53
|
+
Configure logging for your application with optional file output:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import logging
|
|
57
|
+
from puda_drivers.core.logging import setup_logging
|
|
58
|
+
|
|
59
|
+
# Configure logging with file output enabled
|
|
60
|
+
setup_logging(
|
|
61
|
+
enable_file_logging=True,
|
|
62
|
+
log_level=logging.DEBUG,
|
|
63
|
+
logs_folder="logs", # Optional: default to logs
|
|
64
|
+
log_file_name="my_experiment" # Optional: custom log file name
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Or disable file logging (console only)
|
|
68
|
+
setup_logging(
|
|
69
|
+
enable_file_logging=False,
|
|
70
|
+
log_level=logging.INFO
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Logging Options:**
|
|
75
|
+
- `enable_file_logging`: If `True`, logs are written to files in the `logs/` folder. If `False`, logs only go to console (default: `False`)
|
|
76
|
+
- `log_level`: Logging level constant (e.g., `logging.DEBUG`, `logging.INFO`, `logging.WARNING`, `logging.ERROR`, `logging.CRITICAL`) (default: `logging.DEBUG`)
|
|
77
|
+
- `logs_folder`: Name of the folder to store log files (default: `"logs"`)
|
|
78
|
+
- `log_file_name`: Custom name for the log file. If `None` or empty, uses timestamp-based name (e.g., `log_20250101_120000.log`). If provided without `.log` extension, it will be added automatically.
|
|
79
|
+
|
|
80
|
+
When file logging is enabled, logs are saved to timestamped files (unless a custom name is provided) in the `logs/` folder. The logs folder is created automatically if it doesn't exist.
|
|
81
|
+
|
|
50
82
|
### Gantry Control (GCode)
|
|
51
83
|
|
|
52
84
|
```python
|
|
@@ -183,6 +215,29 @@ except ValueError as e:
|
|
|
183
215
|
|
|
184
216
|
Validation errors are automatically logged at the ERROR level before the exception is raised.
|
|
185
217
|
|
|
218
|
+
### Logging Best Practices
|
|
219
|
+
|
|
220
|
+
For production applications, configure logging at the start of your script:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
import logging
|
|
224
|
+
from puda_drivers.core.logging import setup_logging
|
|
225
|
+
from puda_drivers.move import GCodeController
|
|
226
|
+
|
|
227
|
+
# Configure logging first, before initializing devices
|
|
228
|
+
setup_logging(
|
|
229
|
+
enable_file_logging=True,
|
|
230
|
+
log_level=logging.INFO,
|
|
231
|
+
log_file_name="gantry_operation"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Now all device operations will be logged
|
|
235
|
+
gantry = GCodeController(port_name="/dev/ttyACM0")
|
|
236
|
+
# ... rest of your code
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
This ensures all device communication, movements, and errors are captured in log files for debugging and audit purposes.
|
|
240
|
+
|
|
186
241
|
## Finding Serial Ports
|
|
187
242
|
|
|
188
243
|
To discover available serial ports on your system:
|
|
@@ -7,6 +7,7 @@ Hardware drivers for the PUDA (Physical Unified Device Architecture) platform. T
|
|
|
7
7
|
- **Gantry Control**: Control G-code compatible motion systems (e.g., QuBot)
|
|
8
8
|
- **Liquid Handling**: Interface with Sartorius rLINE® pipettes and dispensers
|
|
9
9
|
- **Serial Communication**: Robust serial port management with automatic reconnection
|
|
10
|
+
- **Logging**: Configurable logging with optional file output to logs folder
|
|
10
11
|
- **Cross-platform**: Works on Linux, macOS, and Windows
|
|
11
12
|
|
|
12
13
|
## Installation
|
|
@@ -27,6 +28,37 @@ pip install -e .
|
|
|
27
28
|
|
|
28
29
|
## Quick Start
|
|
29
30
|
|
|
31
|
+
### Logging Configuration
|
|
32
|
+
|
|
33
|
+
Configure logging for your application with optional file output:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import logging
|
|
37
|
+
from puda_drivers.core.logging import setup_logging
|
|
38
|
+
|
|
39
|
+
# Configure logging with file output enabled
|
|
40
|
+
setup_logging(
|
|
41
|
+
enable_file_logging=True,
|
|
42
|
+
log_level=logging.DEBUG,
|
|
43
|
+
logs_folder="logs", # Optional: default to logs
|
|
44
|
+
log_file_name="my_experiment" # Optional: custom log file name
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Or disable file logging (console only)
|
|
48
|
+
setup_logging(
|
|
49
|
+
enable_file_logging=False,
|
|
50
|
+
log_level=logging.INFO
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Logging Options:**
|
|
55
|
+
- `enable_file_logging`: If `True`, logs are written to files in the `logs/` folder. If `False`, logs only go to console (default: `False`)
|
|
56
|
+
- `log_level`: Logging level constant (e.g., `logging.DEBUG`, `logging.INFO`, `logging.WARNING`, `logging.ERROR`, `logging.CRITICAL`) (default: `logging.DEBUG`)
|
|
57
|
+
- `logs_folder`: Name of the folder to store log files (default: `"logs"`)
|
|
58
|
+
- `log_file_name`: Custom name for the log file. If `None` or empty, uses timestamp-based name (e.g., `log_20250101_120000.log`). If provided without `.log` extension, it will be added automatically.
|
|
59
|
+
|
|
60
|
+
When file logging is enabled, logs are saved to timestamped files (unless a custom name is provided) in the `logs/` folder. The logs folder is created automatically if it doesn't exist.
|
|
61
|
+
|
|
30
62
|
### Gantry Control (GCode)
|
|
31
63
|
|
|
32
64
|
```python
|
|
@@ -163,6 +195,29 @@ except ValueError as e:
|
|
|
163
195
|
|
|
164
196
|
Validation errors are automatically logged at the ERROR level before the exception is raised.
|
|
165
197
|
|
|
198
|
+
### Logging Best Practices
|
|
199
|
+
|
|
200
|
+
For production applications, configure logging at the start of your script:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
import logging
|
|
204
|
+
from puda_drivers.core.logging import setup_logging
|
|
205
|
+
from puda_drivers.move import GCodeController
|
|
206
|
+
|
|
207
|
+
# Configure logging first, before initializing devices
|
|
208
|
+
setup_logging(
|
|
209
|
+
enable_file_logging=True,
|
|
210
|
+
log_level=logging.INFO,
|
|
211
|
+
log_file_name="gantry_operation"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Now all device operations will be logged
|
|
215
|
+
gantry = GCodeController(port_name="/dev/ttyACM0")
|
|
216
|
+
# ... rest of your code
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
This ensures all device communication, movements, and errors are captured in log files for debugging and audit purposes.
|
|
220
|
+
|
|
166
221
|
## Finding Serial Ports
|
|
167
222
|
|
|
168
223
|
To discover available serial ports on your system:
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging configuration utility.
|
|
3
|
+
|
|
4
|
+
This module provides a function to configure logging with optional file output
|
|
5
|
+
to a logs folder.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def setup_logging(
|
|
15
|
+
enable_file_logging: bool = False,
|
|
16
|
+
log_level: int = logging.DEBUG,
|
|
17
|
+
logs_folder: str = "logs",
|
|
18
|
+
log_file_name: Optional[str] = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Configure logging with optional file output to a logs folder.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
enable_file_logging: If True, logs will be written to files in the logs folder.
|
|
25
|
+
If False, logs will only be output to console.
|
|
26
|
+
log_level: Logging level constant from logging module (e.g., logging.DEBUG,
|
|
27
|
+
logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL).
|
|
28
|
+
Defaults to logging.DEBUG.
|
|
29
|
+
logs_folder: Name of the folder to store log files (default: "logs")
|
|
30
|
+
log_file_name: Custom name for the log file. If None or empty string, uses
|
|
31
|
+
timestamp-based name. If provided without .log extension, it will
|
|
32
|
+
be added automatically.
|
|
33
|
+
"""
|
|
34
|
+
# Create logs folder if file logging is enabled
|
|
35
|
+
if enable_file_logging:
|
|
36
|
+
log_dir = Path(logs_folder)
|
|
37
|
+
log_dir.mkdir(exist_ok=True)
|
|
38
|
+
|
|
39
|
+
# Create a log file with custom name or timestamp
|
|
40
|
+
if log_file_name and log_file_name.strip(): # None or empty/whitespace strings use timestamp
|
|
41
|
+
# Ensure .log extension if not present
|
|
42
|
+
if not log_file_name.endswith(".log"):
|
|
43
|
+
log_file_name = f"{log_file_name}.log"
|
|
44
|
+
log_file = log_dir / log_file_name
|
|
45
|
+
else:
|
|
46
|
+
# Default: timestamp-based name
|
|
47
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
48
|
+
log_file = log_dir / f"log_{timestamp}.log"
|
|
49
|
+
|
|
50
|
+
# Configure logging with both console and file handlers
|
|
51
|
+
logging.basicConfig(
|
|
52
|
+
level=log_level,
|
|
53
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
54
|
+
handlers=[
|
|
55
|
+
logging.StreamHandler(), # Console output
|
|
56
|
+
logging.FileHandler(log_file, mode="w"), # File output
|
|
57
|
+
],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Log that file logging is enabled
|
|
61
|
+
logger = logging.getLogger(__name__)
|
|
62
|
+
logger.info("File logging enabled. Log file: %s", log_file)
|
|
63
|
+
else:
|
|
64
|
+
# Configure logging with only console handler
|
|
65
|
+
logging.basicConfig(
|
|
66
|
+
level=log_level,
|
|
67
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
68
|
+
handlers=[logging.StreamHandler()], # Console output only
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Log that file logging is disabled
|
|
72
|
+
logger = logging.getLogger(__name__)
|
|
73
|
+
logger.info("File logging disabled. Logs will only be output to console.")
|
|
@@ -5,7 +5,7 @@ Generic Serial Controller for communicating with devices over serial ports.
|
|
|
5
5
|
import time
|
|
6
6
|
import logging
|
|
7
7
|
from typing import Optional, List, Tuple
|
|
8
|
-
from abc import ABC
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
9
|
import serial
|
|
10
10
|
import serial.tools.list_ports
|
|
11
11
|
|
|
@@ -159,9 +159,13 @@ class SerialController(ABC):
|
|
|
159
159
|
# Read all available bytes
|
|
160
160
|
response += self._serial.read(self._serial.in_waiting)
|
|
161
161
|
|
|
162
|
-
# Check for expected response markers for early return
|
|
162
|
+
# Check for expected response markers for early return for qubot
|
|
163
163
|
if b"ok" in response or b"err" in response:
|
|
164
164
|
break
|
|
165
|
+
|
|
166
|
+
# Check for expected response markers for early return for sartorius
|
|
167
|
+
if b"\xba\r" in response:
|
|
168
|
+
break
|
|
165
169
|
else:
|
|
166
170
|
time.sleep(0.1)
|
|
167
171
|
|
|
@@ -175,18 +179,27 @@ class SerialController(ABC):
|
|
|
175
179
|
# Decode once and check the decoded string
|
|
176
180
|
decoded_response = response.decode("utf-8", errors="ignore").strip()
|
|
177
181
|
|
|
178
|
-
if "ok" in decoded_response.lower():
|
|
182
|
+
if "ok" in decoded_response.lower(): # for qubot
|
|
179
183
|
self._logger.debug("<- Received response: %r", decoded_response)
|
|
180
|
-
elif "err" in decoded_response.lower():
|
|
184
|
+
elif "err" in decoded_response.lower(): # for qubot
|
|
181
185
|
self._logger.error("<- Received error: %r", decoded_response)
|
|
186
|
+
elif "º" in decoded_response: # for sartorius
|
|
187
|
+
self._logger.debug("<- Received response: %r", decoded_response)
|
|
182
188
|
else:
|
|
183
189
|
self._logger.warning(
|
|
184
190
|
"<- Received unexpected response (no 'ok' or 'err'): %r", decoded_response
|
|
185
191
|
)
|
|
186
192
|
|
|
187
193
|
return decoded_response
|
|
194
|
+
|
|
195
|
+
@abstractmethod
|
|
196
|
+
def _build_command(self, command: str, value: Optional[str] = None) -> str:
|
|
197
|
+
"""
|
|
198
|
+
Build a command string according to the device protocol.
|
|
199
|
+
"""
|
|
200
|
+
pass
|
|
188
201
|
|
|
189
|
-
def execute(self, command: str) -> str:
|
|
202
|
+
def execute(self, command: str, value: Optional[str] = None) -> str:
|
|
190
203
|
"""
|
|
191
204
|
Send a command and read the response atomically.
|
|
192
205
|
|
|
@@ -205,5 +218,5 @@ class SerialController(ABC):
|
|
|
205
218
|
serial.SerialException: If device is not connected or communication fails
|
|
206
219
|
serial.SerialTimeoutException: If no response is received within timeout
|
|
207
220
|
"""
|
|
208
|
-
self._send_command(command)
|
|
221
|
+
self._send_command(self._build_command(command, value))
|
|
209
222
|
return self._read_response()
|
|
@@ -143,7 +143,7 @@ class GCodeController(SerialController):
|
|
|
143
143
|
self._feed = new_feed
|
|
144
144
|
self._logger.debug("Feed rate set to: %s mm/min.", self._feed)
|
|
145
145
|
|
|
146
|
-
def _build_command(self, command: str) -> str:
|
|
146
|
+
def _build_command(self, command: str, value: Optional[str] = None) -> str:
|
|
147
147
|
"""
|
|
148
148
|
Build a G-code command with terminator.
|
|
149
149
|
|
|
@@ -155,14 +155,14 @@ class GCodeController(SerialController):
|
|
|
155
155
|
"""
|
|
156
156
|
return f"{command}{self.PROTOCOL_TERMINATOR}"
|
|
157
157
|
|
|
158
|
-
def
|
|
158
|
+
def _wait_for_move(self) -> None:
|
|
159
159
|
"""
|
|
160
160
|
Wait for the current move to complete (M400 command).
|
|
161
161
|
|
|
162
162
|
This sends the M400 command which waits for all moves in the queue to complete
|
|
163
163
|
before continuing. This ensures that position updates are accurate.
|
|
164
164
|
"""
|
|
165
|
-
self.execute(
|
|
165
|
+
self.execute("M400")
|
|
166
166
|
|
|
167
167
|
def _validate_axis(self, axis: str) -> str:
|
|
168
168
|
"""
|
|
@@ -305,8 +305,8 @@ class GCodeController(SerialController):
|
|
|
305
305
|
home_target = "All"
|
|
306
306
|
|
|
307
307
|
self._logger.info("[%s] homing axis/axes: %s **", cmd, home_target)
|
|
308
|
-
self.execute(
|
|
309
|
-
self._logger.info("Homing of %s completed
|
|
308
|
+
self.execute(cmd)
|
|
309
|
+
self._logger.info("Homing of %s completed.\n", home_target)
|
|
310
310
|
|
|
311
311
|
# Update internal position (optimistic zeroing)
|
|
312
312
|
if axis:
|
|
@@ -447,17 +447,17 @@ class GCodeController(SerialController):
|
|
|
447
447
|
raise ValueError("Move command issued with both Z and A movement. This is not supported.")
|
|
448
448
|
|
|
449
449
|
# Step 0: Ensure absolute mode is active
|
|
450
|
-
self.execute(
|
|
450
|
+
self.execute("G90")
|
|
451
451
|
needs_xy_move = needs_x_move or needs_y_move
|
|
452
452
|
|
|
453
453
|
# Step 1: Move Z and A to SAFE_MOVE_HEIGHT if XY movement is needed
|
|
454
454
|
if needs_xy_move:
|
|
455
|
-
self._logger.
|
|
455
|
+
self._logger.debug(
|
|
456
456
|
"Safe move: Raising Z and A to safe height (%s) before XY movement", self.SAFE_MOVE_HEIGHT
|
|
457
457
|
)
|
|
458
458
|
move_cmd = f"G1 Z-5 A-5 F{self._z_feed}"
|
|
459
|
-
self.execute(
|
|
460
|
-
self.
|
|
459
|
+
self.execute(move_cmd)
|
|
460
|
+
self._wait_for_move()
|
|
461
461
|
self._current_position["Z"] = self.SAFE_MOVE_HEIGHT
|
|
462
462
|
self._current_position["A"] = self.SAFE_MOVE_HEIGHT
|
|
463
463
|
self._logger.debug("Z and A moved to safe height (%s)", self.SAFE_MOVE_HEIGHT)
|
|
@@ -471,9 +471,9 @@ class GCodeController(SerialController):
|
|
|
471
471
|
move_cmd += f" Y{position['Y']}"
|
|
472
472
|
move_cmd += f" F{feed}"
|
|
473
473
|
|
|
474
|
-
self._logger.
|
|
475
|
-
self.execute(
|
|
476
|
-
self.
|
|
474
|
+
self._logger.debug("Executing XY move command: %s", move_cmd)
|
|
475
|
+
self.execute(move_cmd)
|
|
476
|
+
self._wait_for_move()
|
|
477
477
|
|
|
478
478
|
# Update position for moved axes
|
|
479
479
|
if needs_x_move:
|
|
@@ -484,21 +484,20 @@ class GCodeController(SerialController):
|
|
|
484
484
|
# Step 3: Move Z and A back to original position (or target if specified)
|
|
485
485
|
if needs_z_move:
|
|
486
486
|
move_cmd = f"G1 Z{position['Z']} F{self._z_feed}"
|
|
487
|
-
self.execute(
|
|
487
|
+
self.execute(move_cmd)
|
|
488
488
|
self._current_position["Z"] = position['Z']
|
|
489
489
|
elif needs_a_move:
|
|
490
490
|
move_cmd = f"G1 A{position['A']} F{self._z_feed}"
|
|
491
|
-
self.execute(
|
|
491
|
+
self.execute(move_cmd)
|
|
492
492
|
self._current_position["A"] = position['A']
|
|
493
|
-
self.
|
|
494
|
-
|
|
495
|
-
self._logger.info(
|
|
496
|
-
"Move complete. Final position: %s", self._current_position
|
|
497
|
-
)
|
|
493
|
+
self._wait_for_move()
|
|
498
494
|
self._logger.debug("New internal position: %s", self._current_position)
|
|
499
495
|
|
|
500
496
|
# Step 4: Post-move position synchronization check
|
|
501
|
-
self.
|
|
497
|
+
self._sync_position()
|
|
498
|
+
self._logger.info(
|
|
499
|
+
"Move complete. Final position: %s\n", self._current_position
|
|
500
|
+
)
|
|
502
501
|
|
|
503
502
|
return self._current_position
|
|
504
503
|
|
|
@@ -513,7 +512,7 @@ class GCodeController(SerialController):
|
|
|
513
512
|
Returns an empty dictionary if the query fails or no positions are found.
|
|
514
513
|
"""
|
|
515
514
|
self._logger.info("Querying current machine position (M114).")
|
|
516
|
-
res: str = self.execute(
|
|
515
|
+
res: str = self.execute("M114")
|
|
517
516
|
|
|
518
517
|
# Extract position values using regex
|
|
519
518
|
pattern = re.compile(r"([XYZA]):(-?\d+\.\d+)")
|
|
@@ -532,9 +531,10 @@ class GCodeController(SerialController):
|
|
|
532
531
|
)
|
|
533
532
|
continue
|
|
534
533
|
|
|
534
|
+
self._logger.info("Query position complete. Retrieved positions: %s", position_data)
|
|
535
535
|
return position_data
|
|
536
536
|
|
|
537
|
-
def
|
|
537
|
+
def _sync_position(self) -> Tuple[bool, Dict[str, float]]:
|
|
538
538
|
"""
|
|
539
539
|
Synchronize internal position with actual machine position.
|
|
540
540
|
|
|
@@ -592,7 +592,7 @@ class GCodeController(SerialController):
|
|
|
592
592
|
self._logger.info("Synchronization move successfully completed.")
|
|
593
593
|
|
|
594
594
|
# Recursive call to verify position after move
|
|
595
|
-
return self.
|
|
595
|
+
return self._sync_position()
|
|
596
596
|
except (ValueError, RuntimeError, OSError) as e:
|
|
597
597
|
self._logger.error("Synchronization move failed: %s", e)
|
|
598
598
|
adjustment_needed = False
|
|
@@ -614,7 +614,7 @@ class GCodeController(SerialController):
|
|
|
614
614
|
Machine information string from the device
|
|
615
615
|
"""
|
|
616
616
|
self._logger.info("Querying machine information (M115).")
|
|
617
|
-
return self.execute(
|
|
617
|
+
return self.execute("M115")
|
|
618
618
|
|
|
619
619
|
def get_internal_position(self) -> Dict[str, float]:
|
|
620
620
|
"""
|
{puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/transfer/liquid/sartorius/sartorius.py
RENAMED
|
@@ -72,7 +72,7 @@ class SartoriusController(SerialController):
|
|
|
72
72
|
timeout,
|
|
73
73
|
)
|
|
74
74
|
|
|
75
|
-
def _build_command(self,
|
|
75
|
+
def _build_command(self, command: str, value: Optional[str] = None) -> str:
|
|
76
76
|
"""
|
|
77
77
|
Build a command string according to the Sartorius protocol.
|
|
78
78
|
|
|
@@ -88,28 +88,11 @@ class SartoriusController(SerialController):
|
|
|
88
88
|
return (
|
|
89
89
|
self.PROTOCOL_SOH
|
|
90
90
|
+ self.SLAVE_ADDRESS
|
|
91
|
-
+
|
|
92
|
-
+
|
|
93
|
-
+ value
|
|
91
|
+
+ command
|
|
92
|
+
+ (f"{value}" if value else "")
|
|
94
93
|
+ self.PROTOCOL_TERMINATOR
|
|
95
94
|
)
|
|
96
95
|
|
|
97
|
-
def _check_response_error(self, response: str, operation: str) -> None:
|
|
98
|
-
"""
|
|
99
|
-
Check if a response contains an error and raise an exception if so.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
response: Response string from the device
|
|
103
|
-
operation: Description of the operation being performed (for error message)
|
|
104
|
-
|
|
105
|
-
Raises:
|
|
106
|
-
SartoriusDeviceError: If the response contains an error
|
|
107
|
-
"""
|
|
108
|
-
if self.ERROR_RESPONSE in response.lower():
|
|
109
|
-
raise SartoriusDeviceError(
|
|
110
|
-
f"{operation} failed. Device returned error: {response}"
|
|
111
|
-
)
|
|
112
|
-
|
|
113
96
|
def _validate_speed(self, speed: int, direction: str = "speed") -> None:
|
|
114
97
|
"""
|
|
115
98
|
Validate that a speed value is within the allowed range.
|
|
@@ -149,29 +132,6 @@ class SartoriusController(SerialController):
|
|
|
149
132
|
)
|
|
150
133
|
return value_str
|
|
151
134
|
|
|
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
135
|
def initialize(self) -> None:
|
|
176
136
|
"""
|
|
177
137
|
Initialize the pipette unit (RZ command).
|
|
@@ -183,8 +143,8 @@ class SartoriusController(SerialController):
|
|
|
183
143
|
SartoriusDeviceError: If initialization fails
|
|
184
144
|
"""
|
|
185
145
|
self._logger.info("** Initializing Pipette Head (RZ) **")
|
|
186
|
-
self.
|
|
187
|
-
self._logger.info("** Pipette Initialization Complete
|
|
146
|
+
self.execute(command="RZ")
|
|
147
|
+
self._logger.info("** Pipette Initialization Complete **\n")
|
|
188
148
|
|
|
189
149
|
def get_inward_speed(self) -> int:
|
|
190
150
|
"""
|
|
@@ -197,7 +157,7 @@ class SartoriusController(SerialController):
|
|
|
197
157
|
SartoriusDeviceError: If the query fails
|
|
198
158
|
"""
|
|
199
159
|
self._logger.info("** Querying Inward Speed (DI) **")
|
|
200
|
-
response = self.
|
|
160
|
+
response = self.execute(command="DI")
|
|
201
161
|
|
|
202
162
|
if len(response) < 2:
|
|
203
163
|
raise SartoriusDeviceError(
|
|
@@ -205,7 +165,7 @@ class SartoriusController(SerialController):
|
|
|
205
165
|
)
|
|
206
166
|
|
|
207
167
|
speed = int(response[1])
|
|
208
|
-
self._logger.info("** Current Inward Speed: %s
|
|
168
|
+
self._logger.info("** Current Inward Speed: %s **\n", speed)
|
|
209
169
|
return speed
|
|
210
170
|
|
|
211
171
|
def set_inward_speed(self, speed: int) -> None:
|
|
@@ -221,8 +181,8 @@ class SartoriusController(SerialController):
|
|
|
221
181
|
"""
|
|
222
182
|
self._validate_speed(speed, "Inward")
|
|
223
183
|
self._logger.info("** Setting Inward Speed (SI, Speed: %s) **", speed)
|
|
224
|
-
self.
|
|
225
|
-
self._logger.info("** Inward Speed Set to %s Successfully
|
|
184
|
+
self.execute(command="SI", value=str(speed))
|
|
185
|
+
self._logger.info("** Inward Speed Set to %s Successfully **\n", speed)
|
|
226
186
|
|
|
227
187
|
def get_outward_speed(self) -> int:
|
|
228
188
|
"""
|
|
@@ -235,7 +195,7 @@ class SartoriusController(SerialController):
|
|
|
235
195
|
SartoriusDeviceError: If the query fails
|
|
236
196
|
"""
|
|
237
197
|
self._logger.info("** Querying Outward Speed (DO) **")
|
|
238
|
-
response = self.
|
|
198
|
+
response = self.execute(command="DO")
|
|
239
199
|
|
|
240
200
|
if len(response) < 2:
|
|
241
201
|
raise SartoriusDeviceError(
|
|
@@ -243,7 +203,7 @@ class SartoriusController(SerialController):
|
|
|
243
203
|
)
|
|
244
204
|
|
|
245
205
|
speed = int(response[1])
|
|
246
|
-
self._logger.info("** Current Outward Speed: %s
|
|
206
|
+
self._logger.info("** Current Outward Speed: %s **\n", speed)
|
|
247
207
|
return speed
|
|
248
208
|
|
|
249
209
|
def set_outward_speed(self, speed: int) -> None:
|
|
@@ -259,8 +219,8 @@ class SartoriusController(SerialController):
|
|
|
259
219
|
"""
|
|
260
220
|
self._validate_speed(speed, "Outward")
|
|
261
221
|
self._logger.info("** Setting Outward Speed (SO, Speed: %s) **", speed)
|
|
262
|
-
self.
|
|
263
|
-
self._logger.info("** Outward Speed Set to %s Successfully
|
|
222
|
+
self.execute(command="SO", value=str(speed))
|
|
223
|
+
self._logger.info("** Outward Speed Set to %s Successfully **\n", speed)
|
|
264
224
|
|
|
265
225
|
def run_to_position(self, position: int) -> None:
|
|
266
226
|
"""
|
|
@@ -275,9 +235,10 @@ class SartoriusController(SerialController):
|
|
|
275
235
|
"""
|
|
276
236
|
position_str = self._validate_no_leading_zeros(position, "RP")
|
|
277
237
|
self._logger.info("** Run to absolute Position (RP, Position: %s) **", position)
|
|
278
|
-
self.
|
|
279
|
-
self._logger.info("** Reached Position %s Successfully
|
|
280
|
-
|
|
238
|
+
self.execute(command="RP", value=position_str)
|
|
239
|
+
self._logger.info("** Reached Position %s Successfully **\n", position)
|
|
240
|
+
|
|
241
|
+
# instead of run_inward, use aspirate
|
|
281
242
|
def aspirate(self, amount: float) -> None:
|
|
282
243
|
"""
|
|
283
244
|
Aspirate fluid from the current location.
|
|
@@ -294,9 +255,10 @@ class SartoriusController(SerialController):
|
|
|
294
255
|
|
|
295
256
|
steps = int(amount / self.MICROLITER_PER_STEP)
|
|
296
257
|
self._logger.info("** Aspirating %s uL (RI%s steps) **", amount, steps)
|
|
297
|
-
self.
|
|
298
|
-
self._logger.info("** Aspirated %s uL Successfully
|
|
258
|
+
self.execute(command="RI", value=str(steps))
|
|
259
|
+
self._logger.info("** Aspirated %s uL Successfully **\n", amount)
|
|
299
260
|
|
|
261
|
+
# instead of run_outward, use dispense
|
|
300
262
|
def dispense(self, amount: float) -> None:
|
|
301
263
|
"""
|
|
302
264
|
Dispense fluid at the current location.
|
|
@@ -313,8 +275,8 @@ class SartoriusController(SerialController):
|
|
|
313
275
|
|
|
314
276
|
steps = int(amount / self.MICROLITER_PER_STEP)
|
|
315
277
|
self._logger.info("** Dispensing %s uL (RO%s steps) **", amount, steps)
|
|
316
|
-
self.
|
|
317
|
-
self._logger.info("** Dispensed %s uL Successfully
|
|
278
|
+
self.execute(command="RO", value=str(steps))
|
|
279
|
+
self._logger.info("** Dispensed %s uL Successfully **\n", amount)
|
|
318
280
|
|
|
319
281
|
def eject_tip(self, return_position: int = 30) -> None:
|
|
320
282
|
"""
|
|
@@ -333,10 +295,8 @@ class SartoriusController(SerialController):
|
|
|
333
295
|
return_position,
|
|
334
296
|
return_position,
|
|
335
297
|
)
|
|
336
|
-
self.
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
self._logger.info("** Tip Ejection Complete **")
|
|
298
|
+
self.execute(command="RE", value=position_str)
|
|
299
|
+
self._logger.info("** Tip Ejection Complete **\n")
|
|
340
300
|
|
|
341
301
|
def run_blowout(self, return_position: Optional[int] = None) -> None:
|
|
342
302
|
"""
|
|
@@ -359,14 +319,12 @@ class SartoriusController(SerialController):
|
|
|
359
319
|
return_position,
|
|
360
320
|
return_position,
|
|
361
321
|
)
|
|
362
|
-
self.
|
|
363
|
-
"B", value=position_str, operation="Blowout with return position"
|
|
364
|
-
)
|
|
322
|
+
self.execute(command="RB", value=position_str)
|
|
365
323
|
else:
|
|
366
324
|
self._logger.info("** Running Blowout (RB) **")
|
|
367
|
-
self.
|
|
325
|
+
self.execute(command="RB")
|
|
368
326
|
|
|
369
|
-
self._logger.info("** Blowout Complete
|
|
327
|
+
self._logger.info("** Blowout Complete **\n")
|
|
370
328
|
|
|
371
329
|
def get_status(self) -> str:
|
|
372
330
|
"""
|
|
@@ -379,7 +337,7 @@ class SartoriusController(SerialController):
|
|
|
379
337
|
SartoriusDeviceError: If the status query fails
|
|
380
338
|
"""
|
|
381
339
|
self._logger.info("** Querying Pipette Status (DS) **")
|
|
382
|
-
response = self.
|
|
340
|
+
response = self.execute(command="DS")
|
|
383
341
|
|
|
384
342
|
if len(response) < 2:
|
|
385
343
|
raise SartoriusDeviceError(
|
|
@@ -389,10 +347,37 @@ class SartoriusController(SerialController):
|
|
|
389
347
|
status_code = response[1]
|
|
390
348
|
if status_code in STATUS_CODES:
|
|
391
349
|
status_message = STATUS_CODES[status_code]
|
|
392
|
-
self._logger.info("Pipette Status Code [%s]: %s", status_code, status_message)
|
|
350
|
+
self._logger.info("Pipette Status Code [%s]: %s\n", status_code, status_message)
|
|
393
351
|
else:
|
|
394
352
|
self._logger.warning(
|
|
395
|
-
"Pipette Status Code [%s]: Unknown Status Code", status_code
|
|
353
|
+
"Pipette Status Code [%s]: Unknown Status Code\n", status_code
|
|
396
354
|
)
|
|
397
355
|
|
|
398
356
|
return status_code
|
|
357
|
+
|
|
358
|
+
def get_position(self) -> int:
|
|
359
|
+
"""
|
|
360
|
+
Query the current position of the pipette (DP command).
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Current position in steps
|
|
364
|
+
"""
|
|
365
|
+
self._logger.info("** Querying Position (DP) **")
|
|
366
|
+
response = self.execute(command="DP")
|
|
367
|
+
self._logger.info("** Position: %s steps **\n", response)
|
|
368
|
+
return response
|
|
369
|
+
|
|
370
|
+
def get_liquid_level(self) -> int:
|
|
371
|
+
"""
|
|
372
|
+
Query the current liquid level of the pipette (DN command).
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Current liquid level in microliters (µL)
|
|
376
|
+
"""
|
|
377
|
+
# without tip 240 - 300
|
|
378
|
+
# incrase with tip attached and liquid
|
|
379
|
+
# 160 - 400
|
|
380
|
+
self._logger.info("** Querying Liquid Level (DN) **")
|
|
381
|
+
response = self.execute(command="DN")
|
|
382
|
+
self._logger.info("** Liquid Level: %s uL **\n", response)
|
|
383
|
+
return response
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from puda_drivers.transfer.liquid.sartorius import SartoriusController
|
|
3
|
+
from puda_drivers.core.logging import setup_logging
|
|
3
4
|
|
|
4
5
|
# Optional: finding ports
|
|
5
6
|
# import serial.tools.list_ports
|
|
6
7
|
# for port, desc, hwid in serial.tools.list_ports.comports():
|
|
7
8
|
# print(f"{port}: {desc} [{hwid}]")
|
|
8
9
|
|
|
9
|
-
#
|
|
10
|
-
# All loggers in imported modules (SerialController,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# Recommended format: includes time, logger name, level, and message
|
|
15
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
10
|
+
# --- LOGGING CONFIGURATION ---
|
|
11
|
+
# All loggers in imported modules (SerialController, SartoriusController) will inherit this setup.
|
|
12
|
+
setup_logging(
|
|
13
|
+
enable_file_logging=False,
|
|
14
|
+
log_level=logging.DEBUG, # Use logging.DEBUG to see all (DEBUG, INFO, WARNING, ERROR, CRITICAL) logs
|
|
16
15
|
)
|
|
17
16
|
|
|
18
|
-
#
|
|
19
|
-
# logging.getLogger('
|
|
17
|
+
# OPTIONAL: If you only want specific loggers at specific level, you can specifically set it here
|
|
18
|
+
# logging.getLogger('puda_drivers.transfer.liquid.sartorius').setLevel(logging.INFO)
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
# --- CONFIGURATION ---
|
|
@@ -36,25 +35,34 @@ def test_pipette_operations():
|
|
|
36
35
|
|
|
37
36
|
try:
|
|
38
37
|
# 1. Initialize and Connect
|
|
39
|
-
print("
|
|
38
|
+
print("[STEP 1] Connecting to pipette...")
|
|
40
39
|
# SartoriusController connects automatically in __init__, no need to call connect()
|
|
41
|
-
|
|
40
|
+
|
|
42
41
|
# Always start with initializing
|
|
43
42
|
pipette.initialize()
|
|
44
43
|
|
|
44
|
+
pipette.get_status()
|
|
45
|
+
pipette.get_liquid_level()
|
|
46
|
+
|
|
47
|
+
# 2. Set and get inward and outward speeds
|
|
48
|
+
print("[STEP 2] Setting and getting inward and outward speeds...")
|
|
49
|
+
pipette.set_inward_speed(3)
|
|
45
50
|
pipette.get_inward_speed()
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
|
|
52
|
+
pipette.set_outward_speed(3)
|
|
53
|
+
pipette.get_outward_speed()
|
|
54
|
+
pipette.run_to_position(100)
|
|
55
|
+
pipette.get_position()
|
|
48
56
|
|
|
49
57
|
# 3. Eject Tip (if any)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
print("[STEP 3] Ejecting Tip (if any)...")
|
|
59
|
+
pipette.eject_tip(return_position=30)
|
|
60
|
+
print(f"[STEP 3] Aspirate {TRANSFER_VOLUME} uL...")
|
|
61
|
+
pipette.aspirate(amount=TRANSFER_VOLUME)
|
|
54
62
|
|
|
55
63
|
# 4. Dispense
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
print(f"[STEP 4] Dispensing {TRANSFER_VOLUME} uL...")
|
|
65
|
+
pipette.dispense(amount=TRANSFER_VOLUME)
|
|
58
66
|
|
|
59
67
|
# # 5. Eject Tip
|
|
60
68
|
# print("\n[STEP 5] Ejecting Tip...")
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from puda_drivers.move import GCodeController
|
|
3
|
+
from puda_drivers.core.logging import setup_logging
|
|
3
4
|
|
|
4
5
|
# Optinal: finding ports
|
|
5
|
-
import serial.tools.list_ports
|
|
6
|
-
for port, desc, hwid in serial.tools.list_ports.comports():
|
|
7
|
-
|
|
6
|
+
# import serial.tools.list_ports
|
|
7
|
+
# for port, desc, hwid in serial.tools.list_ports.comports():
|
|
8
|
+
# print(f"{port}: {desc} [{hwid}]")
|
|
8
9
|
|
|
9
|
-
#
|
|
10
|
+
# --- LOGGING CONFIGURATION ---
|
|
10
11
|
# All loggers in imported modules (SerialController, GCodeController) will inherit this setup.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# Recommended format: includes time, logger name, level, and message
|
|
15
|
-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
12
|
+
setup_logging(
|
|
13
|
+
enable_file_logging=True,
|
|
14
|
+
log_level=logging.INFO, # Use logging.DEBUG to see all (DEBUG, INFO, WARNING, ERROR, CRITICAL) logs
|
|
16
15
|
)
|
|
17
16
|
|
|
18
|
-
#
|
|
17
|
+
# OPTIONAL: If you only want GCodeController's logs at specific level, you can specifically set it here
|
|
19
18
|
# logging.getLogger('puda_drivers.gcodecontroller').setLevel(logging.INFO)
|
|
20
19
|
|
|
21
20
|
PORT_NAME = "/dev/ttyACM0"
|
|
@@ -46,7 +45,6 @@ def main():
|
|
|
46
45
|
|
|
47
46
|
# qubot.query_position()
|
|
48
47
|
# Always start with homing
|
|
49
|
-
print("\n")
|
|
50
48
|
qubot.home()
|
|
51
49
|
|
|
52
50
|
# # Setting feed rate (aka move speed)
|
|
@@ -54,26 +52,22 @@ def main():
|
|
|
54
52
|
# qubot.feed = 5000
|
|
55
53
|
|
|
56
54
|
# Relative moves are converted to absolute internally, but works the same
|
|
57
|
-
# for anything in the
|
|
58
|
-
print("\n")
|
|
55
|
+
# for anything in the -axis, will have to be moved individually, else error will be raised
|
|
59
56
|
qubot.move_absolute(x=00.0, y=-50.0, a=-100.0)
|
|
60
57
|
|
|
61
58
|
# print("\n")
|
|
62
59
|
# qubot.move_relative(x=10.0)
|
|
63
60
|
|
|
64
61
|
# Example stepping code
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
for _ in range(10):
|
|
63
|
+
pos = qubot.move_relative(x=10.0)
|
|
64
|
+
print(f"Position: {pos}")
|
|
68
65
|
|
|
69
|
-
# sync position
|
|
70
|
-
#
|
|
66
|
+
# sync position is always called after move automatically (now private: _sync_position())
|
|
67
|
+
# Position synchronization happens automatically after each move
|
|
71
68
|
|
|
72
|
-
# print("\n")
|
|
73
69
|
# qubot.move_absolute(x=330.0, y=-440.0, z=-175.0)
|
|
74
|
-
|
|
75
|
-
# print("\n")
|
|
76
|
-
# qubot.sync_position()
|
|
70
|
+
# Position synchronization happens automatically after each move
|
|
77
71
|
# Example of an ERROR - invalid axis
|
|
78
72
|
# try:
|
|
79
73
|
# qubot.home(axis="B") # Generates ERROR
|
|
@@ -100,6 +94,9 @@ def main():
|
|
|
100
94
|
# qubot.move_absolute(z=-10.0, a=-20.0) # Raises ValueError if both Z and A are moved
|
|
101
95
|
# except ValueError as e:
|
|
102
96
|
# print(f"Z/A simultaneous movement error (expected): {e}")
|
|
97
|
+
|
|
98
|
+
qubot.disconnect()
|
|
99
|
+
print("Disconnected from qubot")
|
|
103
100
|
|
|
104
101
|
except Exception as e:
|
|
105
102
|
logging.getLogger(__name__).error("An unrecoverable error occurred: %s", e)
|
|
@@ -2,11 +2,12 @@ import logging
|
|
|
2
2
|
import serial
|
|
3
3
|
from puda_drivers.move import GCodeController
|
|
4
4
|
from puda_drivers.transfer.liquid.sartorius import SartoriusController
|
|
5
|
+
from puda_drivers.core.logging import setup_logging
|
|
5
6
|
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
# --- LOGGING CONFIGURATION ---
|
|
8
|
+
setup_logging(
|
|
9
|
+
enable_file_logging=False,
|
|
10
|
+
log_level=logging.INFO, # Use INFO level for cleaner output during automation
|
|
10
11
|
)
|
|
11
12
|
|
|
12
13
|
# Qubot Configuration
|
|
@@ -65,6 +66,7 @@ try:
|
|
|
65
66
|
print("Connecting to pipette")
|
|
66
67
|
# SartoriusController connects automatically in __init__, no need to call connect()
|
|
67
68
|
pipette = SartoriusController(port_name=SARTORIUS_PORT)
|
|
69
|
+
# always start with initializing
|
|
68
70
|
pipette.initialize()
|
|
69
71
|
except Exception as e:
|
|
70
72
|
print(f"FATAL ERROR: Could not initialize/connect Sartorius: {e}")
|
|
@@ -90,10 +92,6 @@ def run_automation():
|
|
|
90
92
|
# 4. Protocol Step 2: Aspirate Liquid
|
|
91
93
|
print(f"\nProtocol Step 2: Aspirating {TRANSFER_VOLUME} uL")
|
|
92
94
|
|
|
93
|
-
# Move to safe Z-height before moving across the deck
|
|
94
|
-
pos = qubot.move_absolute(z=SAFE_Z_POS)
|
|
95
|
-
print(f" Position after safe Z move: {pos}")
|
|
96
|
-
|
|
97
95
|
# Move to the source well (ASPIRATE_POS)
|
|
98
96
|
pos = qubot.move_absolute(
|
|
99
97
|
x=ASPIRATE_POS["X"], y=ASPIRATE_POS["Y"], z=ASPIRATE_POS["Z"], feed=3000
|
|
@@ -129,10 +127,6 @@ def run_automation():
|
|
|
129
127
|
# 6. Protocol Step 4: Finalization
|
|
130
128
|
print("\nProtocol Step 4: Finalizing")
|
|
131
129
|
|
|
132
|
-
# Move back to safe Z-height
|
|
133
|
-
pos = qubot.move_absolute(z=SAFE_Z_POS)
|
|
134
|
-
print(f" Position after safe Z move: {pos}")
|
|
135
|
-
|
|
136
130
|
# Simulate moving to a trash bin and ejecting the tip
|
|
137
131
|
pos = qubot.move_absolute(x=10.0, y=-10.0)
|
|
138
132
|
print(f" Position at trash bin: {pos}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/transfer/liquid/sartorius/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{puda_drivers-0.0.5 → puda_drivers-0.0.7}/src/puda_drivers/transfer/liquid/sartorius/constants.py
RENAMED
|
File without changes
|
|
File without changes
|