puda-drivers 0.0.5__tar.gz → 0.0.6__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 (25) hide show
  1. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/.gitignore +4 -0
  2. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/PKG-INFO +56 -1
  3. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/README.md +55 -0
  4. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/pyproject.toml +1 -1
  5. puda_drivers-0.0.6/src/puda_drivers/core/__init__.py +4 -0
  6. puda_drivers-0.0.6/src/puda_drivers/core/logging.py +73 -0
  7. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/src/puda_drivers/core/serialcontroller.py +9 -3
  8. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/tests/pipette.py +19 -16
  9. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/tests/qubot.py +10 -11
  10. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/tests/together.py +6 -12
  11. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/uv.lock +1 -1
  12. puda_drivers-0.0.5/src/puda_drivers/core/__init__.py +0 -3
  13. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/LICENSE +0 -0
  14. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/src/puda_drivers/__init__.py +0 -0
  15. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/src/puda_drivers/move/__init__.py +0 -0
  16. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/src/puda_drivers/move/gcode.py +0 -0
  17. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/src/puda_drivers/move/grbl/__init__.py +0 -0
  18. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/src/puda_drivers/move/grbl/api.py +0 -0
  19. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/src/puda_drivers/move/grbl/constants.py +0 -0
  20. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/src/puda_drivers/py.typed +0 -0
  21. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/src/puda_drivers/transfer/liquid/sartorius/__init__.py +0 -0
  22. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/src/puda_drivers/transfer/liquid/sartorius/api.py +0 -0
  23. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/src/puda_drivers/transfer/liquid/sartorius/constants.py +0 -0
  24. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/src/puda_drivers/transfer/liquid/sartorius/sartorius.py +0 -0
  25. {puda_drivers-0.0.5 → puda_drivers-0.0.6}/tests/example.py +0 -0
@@ -129,3 +129,7 @@ dmypy.json
129
129
 
130
130
  # Pyre type checker
131
131
  .pyre/
132
+
133
+ # Log files
134
+ logs/
135
+ *.log
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: puda-drivers
3
- Version: 0.0.5
3
+ Version: 0.0.6
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:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "puda-drivers"
3
- version = "0.0.5"
3
+ version = "0.0.6"
4
4
  description = "Hardware drivers for the PUDA platform."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -0,0 +1,4 @@
1
+ from .serialcontroller import SerialController, list_serial_ports
2
+ from .logging import setup_logging
3
+
4
+ __all__ = ["SerialController", "list_serial_ports", "setup_logging"]
@@ -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.")
@@ -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,10 +179,12 @@ 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
@@ -1,22 +1,25 @@
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
- # 1. Configure the root logger
10
- # All loggers in imported modules (SerialController, GCodeController) will inherit this setup.
11
- logging.basicConfig(
12
- # Use logging.DEBUG to see all (DEBUG, INFO, WARNING, ERROR, CRITICAL) logs
13
- level=logging.DEBUG,
14
- # Recommended format: includes time, logger name, level, and message
15
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
10
+ # --- LOGGING CONFIGURATION ---
11
+ # Set ENABLE_FILE_LOGGING to True to save logs to logs/ folder, False to only output to console
12
+ ENABLE_FILE_LOGGING = True # Change to False to disable file logging
13
+
14
+ # Configure logging
15
+ # All loggers in imported modules (SerialController, SartoriusController) will inherit this setup.
16
+ setup_logging(
17
+ enable_file_logging=ENABLE_FILE_LOGGING,
18
+ log_level=logging.DEBUG, # Use logging.DEBUG to see all (DEBUG, INFO, WARNING, ERROR, CRITICAL) logs
16
19
  )
17
20
 
18
- # 2. OPTIONAL: If you only want GCodeController's logs at specific level, you can specifically set it here
19
- # logging.getLogger('drivers.gcodecontroller').setLevel(logging.INFO)
21
+ # OPTIONAL: If you only want specific loggers at specific level, you can specifically set it here
22
+ # logging.getLogger('puda_drivers.transfer.liquid.sartorius').setLevel(logging.INFO)
20
23
 
21
24
 
22
25
  # --- CONFIGURATION ---
@@ -42,19 +45,19 @@ def test_pipette_operations():
42
45
  # Always start with initializing
43
46
  pipette.initialize()
44
47
 
45
- pipette.get_inward_speed()
48
+ # pipette.get_inward_speed()
46
49
  # print("\n set inward speed to 3")
47
50
  # pipette.set_inward_speed(3)
48
51
 
49
52
  # 3. Eject Tip (if any)
50
- # print("\n[STEP 3] Ejecting Tip (if any)...")
51
- # pipette.eject_tip(return_position=30)
52
- # print(f"\n[STEP 3] Aspirate {TRANSFER_VOLUME} uL...")
53
- # pipette.aspirate(amount=TRANSFER_VOLUME)
53
+ print("\n[STEP 3] Ejecting Tip (if any)...")
54
+ pipette.eject_tip(return_position=30)
55
+ print(f"\n[STEP 3] Aspirate {TRANSFER_VOLUME} uL...")
56
+ pipette.aspirate(amount=TRANSFER_VOLUME)
54
57
 
55
58
  # 4. Dispense
56
- # print(f"\n[STEP 4] Dispensing {TRANSFER_VOLUME} uL...")
57
- # pipette.dispense(amount=TRANSFER_VOLUME)
59
+ print(f"\n[STEP 4] Dispensing {TRANSFER_VOLUME} uL...")
60
+ pipette.dispense(amount=TRANSFER_VOLUME)
58
61
 
59
62
  # # 5. Eject Tip
60
63
  # 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
- print(f"{port}: {desc} [{hwid}]")
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
- # 1. Configure the root logger
10
+ # --- LOGGING CONFIGURATION ---
10
11
  # All loggers in imported modules (SerialController, GCodeController) will inherit this setup.
11
- logging.basicConfig(
12
- # Use logging.DEBUG to see all (DEBUG, INFO, WARNING, ERROR, CRITICAL) logs
13
- level=logging.WARNING,
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.DEBUG, # Use logging.DEBUG to see all (DEBUG, INFO, WARNING, ERROR, CRITICAL) logs
16
15
  )
17
16
 
18
- # 2. OPTIONAL: If you only want GCodeController's logs at specific level, you can specifically set it here
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"
@@ -54,7 +53,7 @@ def main():
54
53
  # qubot.feed = 5000
55
54
 
56
55
  # Relative moves are converted to absolute internally, but works the same
57
- # for anything in the Z-axis, will have to be moved individually, else error will be raised
56
+ # for anything in the -axis, will have to be moved individually, else error will be raised
58
57
  print("\n")
59
58
  qubot.move_absolute(x=00.0, y=-50.0, a=-100.0)
60
59
 
@@ -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
- # Configure logging
7
- logging.basicConfig(
8
- level=logging.INFO, # Use INFO level for cleaner output during automation
9
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
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}")
@@ -9,7 +9,7 @@ resolution-markers = [
9
9
 
10
10
  [[package]]
11
11
  name = "puda-drivers"
12
- version = "0.0.5"
12
+ version = "0.0.6"
13
13
  source = { editable = "." }
14
14
  dependencies = [
15
15
  { name = "pyserial" },
@@ -1,3 +0,0 @@
1
- from .serialcontroller import SerialController, list_serial_ports
2
-
3
- __all__ = ["SerialController", "list_serial_ports"]
File without changes