puda-drivers 0.0.4__py3-none-any.whl → 0.0.5__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.
@@ -2,12 +2,12 @@
2
2
  Generic Serial Controller for communicating with devices over serial ports.
3
3
  """
4
4
 
5
- import serial
6
5
  import time
7
- import serial.tools.list_ports
8
6
  import logging
9
7
  from typing import Optional, List, Tuple
10
8
  from abc import ABC
9
+ import serial
10
+ import serial.tools.list_ports
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
@@ -45,7 +45,7 @@ def list_serial_ports(filter_desc: Optional[str] = None) -> List[Tuple[str, str,
45
45
 
46
46
  class SerialController(ABC):
47
47
  DEFAULT_BAUDRATE = 9600
48
- DEFAULT_TIMEOUT = 20 # seconds
48
+ DEFAULT_TIMEOUT = 30 # seconds
49
49
  POLL_INTERVAL = 0.1 # seconds
50
50
 
51
51
  def __init__(self, port_name, baudrate=DEFAULT_BAUDRATE, timeout=DEFAULT_TIMEOUT):
@@ -127,8 +127,8 @@ class SerialController(ABC):
127
127
  try:
128
128
  self._serial.reset_input_buffer() # clear input buffer
129
129
  self._serial.reset_output_buffer() # clear output buffer
130
- self._serial.write(bytes(command, "utf-8"))
131
130
  self._serial.flush()
131
+ self._serial.write(bytes(command, "utf-8"))
132
132
 
133
133
  except serial.SerialTimeoutException as e:
134
134
  # Log the timeout error and return None as requested (no re-raise)
@@ -55,7 +55,9 @@ class GCodeController(SerialController):
55
55
 
56
56
  DEFAULT_FEEDRATE = 3000 # mm/min
57
57
  MAX_FEEDRATE = 3000 # mm/min
58
+ MAX_Z_FEED_RATE = 1000 # mm/min
58
59
  TOLERANCE = 0.01 # tolerance for position sync in mm
60
+ SAFE_MOVE_HEIGHT = -5 # safe height for Z and A axes in mm
59
61
 
60
62
  PROTOCOL_TERMINATOR = "\r"
61
63
  VALID_AXES = "XYZA"
@@ -66,6 +68,7 @@ class GCodeController(SerialController):
66
68
  baudrate: int = SerialController.DEFAULT_BAUDRATE,
67
69
  timeout: int = SerialController.DEFAULT_TIMEOUT,
68
70
  feed: int = DEFAULT_FEEDRATE,
71
+ z_feed: int = MAX_Z_FEED_RATE,
69
72
  ):
70
73
  """
71
74
  Initialize the G-code controller.
@@ -87,13 +90,14 @@ class GCodeController(SerialController):
87
90
  )
88
91
 
89
92
  # Tracks internal position state
90
- self.current_position: Dict[str, float] = {
93
+ self._current_position: Dict[str, float] = {
91
94
  "X": 0.0,
92
95
  "Y": 0.0,
93
96
  "Z": 0.0,
94
97
  "A": 0.0,
95
98
  }
96
99
  self._feed: int = feed
100
+ self._z_feed: int = z_feed
97
101
 
98
102
  # Initialize axis limits with default values
99
103
  self._axis_limits: Dict[str, AxisLimits] = {
@@ -151,6 +155,15 @@ class GCodeController(SerialController):
151
155
  """
152
156
  return f"{command}{self.PROTOCOL_TERMINATOR}"
153
157
 
158
+ def wait_for_move(self) -> None:
159
+ """
160
+ Wait for the current move to complete (M400 command).
161
+
162
+ This sends the M400 command which waits for all moves in the queue to complete
163
+ before continuing. This ensures that position updates are accurate.
164
+ """
165
+ self.execute(self._build_command("M400"))
166
+
154
167
  def _validate_axis(self, axis: str) -> str:
155
168
  """
156
169
  Validate and normalize an axis name.
@@ -292,19 +305,19 @@ class GCodeController(SerialController):
292
305
  home_target = "All"
293
306
 
294
307
  self._logger.info("[%s] homing axis/axes: %s **", cmd, home_target)
295
- self._send_command(self._build_command(cmd))
308
+ self.execute(self._build_command(cmd))
296
309
  self._logger.info("Homing of %s completed.", home_target)
297
310
 
298
311
  # Update internal position (optimistic zeroing)
299
312
  if axis:
300
- self.current_position[axis] = 0.0
313
+ self._current_position[axis] = 0.0
301
314
  else:
302
- for key in self.current_position:
303
- self.current_position[key] = 0.0
315
+ for key in self._current_position:
316
+ self._current_position[key] = 0.0
304
317
 
305
318
  self._logger.debug(
306
319
  "Internal position updated (optimistically zeroed) to %s",
307
- self.current_position,
320
+ self._current_position,
308
321
  )
309
322
 
310
323
  def move_absolute(
@@ -314,7 +327,7 @@ class GCodeController(SerialController):
314
327
  z: Optional[float] = None,
315
328
  a: Optional[float] = None,
316
329
  feed: Optional[int] = None,
317
- ) -> None:
330
+ ) -> Dict[str, float]:
318
331
  """
319
332
  Move to an absolute position (G90 + G1 command).
320
333
 
@@ -331,17 +344,26 @@ class GCodeController(SerialController):
331
344
  # Validate positions before executing move
332
345
  self._validate_move_positions(x=x, y=y, z=z, a=a)
333
346
 
347
+ # Fill in missing axes with current positions
348
+ target_x = x if x is not None else self._current_position["X"]
349
+ target_y = y if y is not None else self._current_position["Y"]
350
+ target_z = z if z is not None else self._current_position["Z"]
351
+ target_a = a if a is not None else self._current_position["A"]
352
+
334
353
  feed_rate = feed if feed is not None else self._feed
335
354
  self._logger.info(
336
355
  "Preparing absolute move to X:%s, Y:%s, Z:%s, A:%s at F:%s",
337
- x,
338
- y,
339
- z,
340
- a,
356
+ target_x,
357
+ target_y,
358
+ target_z,
359
+ target_a,
341
360
  feed_rate,
342
361
  )
343
362
 
344
- self._execute_move(x=x, y=y, z=z, a=a, feed=feed)
363
+ return self._execute_move(
364
+ position={"X": target_x, "Y": target_y, "Z": target_z, "A": target_a},
365
+ feed=feed_rate
366
+ )
345
367
 
346
368
  def move_relative(
347
369
  self,
@@ -350,7 +372,7 @@ class GCodeController(SerialController):
350
372
  z: Optional[float] = None,
351
373
  a: Optional[float] = None,
352
374
  feed: Optional[int] = None,
353
- ) -> None:
375
+ ) -> Dict[str, float]:
354
376
  """
355
377
  Move relative to the current position (converted to absolute move internally).
356
378
 
@@ -374,133 +396,111 @@ class GCodeController(SerialController):
374
396
  feed_rate,
375
397
  )
376
398
 
377
- # Convert relative movements to absolute positions
378
- abs_x = (self.current_position["X"] + x) if x is not None else None
379
- abs_y = (self.current_position["Y"] + y) if y is not None else None
380
- abs_z = (self.current_position["Z"] + z) if z is not None else None
381
- abs_a = (self.current_position["A"] + a) if a is not None else None
399
+ # Convert relative movements to absolute positions, filling in missing axes with current position
400
+ abs_x = (self._current_position["X"] + x) if x is not None else self._current_position["X"]
401
+ abs_y = (self._current_position["Y"] + y) if y is not None else self._current_position["Y"]
402
+ abs_z = (self._current_position["Z"] + z) if z is not None else self._current_position["Z"]
403
+ abs_a = (self._current_position["A"] + a) if a is not None else self._current_position["A"]
382
404
 
383
405
  # Validate absolute positions before executing move
384
406
  self._validate_move_positions(x=abs_x, y=abs_y, z=abs_z, a=abs_a)
385
407
 
386
- self._execute_move(x=abs_x, y=abs_y, z=abs_z, a=abs_a, feed=feed)
408
+ return self._execute_move(
409
+ position={"X": abs_x, "Y": abs_y, "Z": abs_z, "A": abs_a},
410
+ feed=feed_rate
411
+ )
387
412
 
388
413
  def _execute_move(
389
414
  self,
390
- x: Optional[float] = None,
391
- y: Optional[float] = None,
392
- z: Optional[float] = None,
393
- a: Optional[float] = None,
394
- feed: Optional[int] = None,
395
- ) -> None:
415
+ position: Dict[str, float],
416
+ feed: int,
417
+ ) -> Dict[str, float]:
396
418
  """
397
419
  Internal helper for executing G1 move commands with safe movement pattern.
398
420
  All coordinates are treated as absolute positions.
399
421
 
400
422
  Safe move pattern:
401
423
  1. If X or Y movement is needed, first move Z to 0 (safe height)
402
- 2. Then move X, Y (and optionally A) to target
403
- 3. Finally move Z to target position (if specified)
424
+ 2. Then move X, Y to target
425
+ 3. Finally move Z and A back to original position (or target if specified)
404
426
 
405
427
  Args:
406
- x: Absolute X position (optional)
407
- y: Absolute Y position (optional)
408
- z: Absolute Z position (optional)
409
- a: Absolute A position (optional)
410
- feed: Feed rate (optional)
411
- """
412
- # Calculate target positions (all absolute)
413
- target_pos = self.current_position.copy()
414
- has_x = x is not None
415
- has_y = y is not None
416
- has_z = z is not None
417
- has_a = a is not None
418
-
419
- if not (has_x or has_y or has_z or has_a):
428
+ position: Dictionary with absolute positions for X, Y, Z, A axes
429
+ feed: Feed rate for the move
430
+ """
431
+ # Check if any movement is needed
432
+ needs_x_move = abs(position["X"] - self._current_position["X"]) > self.TOLERANCE
433
+ needs_y_move = abs(position["Y"] - self._current_position["Y"]) > self.TOLERANCE
434
+ needs_z_move = abs(position["Z"] - self._current_position["Z"]) > self.TOLERANCE
435
+ needs_a_move = abs(position["A"] - self._current_position["A"]) > self.TOLERANCE
436
+
437
+ if not (needs_x_move or needs_y_move or needs_z_move or needs_a_move):
420
438
  self._logger.warning(
421
439
  "Move command issued without any axis movement. Skipping transmission."
422
440
  )
423
441
  return
442
+
443
+ if needs_z_move and needs_a_move:
444
+ self._logger.warning(
445
+ "Move command issued with both Z and A movement. This is not supported. Skipping transmission."
446
+ )
447
+ raise ValueError("Move command issued with both Z and A movement. This is not supported.")
424
448
 
425
- # Set target positions (all absolute)
426
- if has_x:
427
- target_pos["X"] = x
428
- if has_y:
429
- target_pos["Y"] = y
430
- if has_z:
431
- target_pos["Z"] = z
432
- if has_a:
433
- target_pos["A"] = a
434
-
435
- feed_rate = feed if feed is not None else self._feed
436
- if feed_rate > self.MAX_FEEDRATE:
437
- feed_rate = self.MAX_FEEDRATE
438
-
439
- # Ensure absolute mode is active
440
- self._send_command(self._build_command("G90"))
441
-
442
- # Safe move pattern: Z to 0, then XY, then Z to target
443
- needs_xy_move = has_x or has_y
444
- current_z = self.current_position["Z"]
445
- target_z = target_pos["Z"] if has_z else current_z
449
+ # Step 0: Ensure absolute mode is active
450
+ self.execute(self._build_command("G90"))
451
+ needs_xy_move = needs_x_move or needs_y_move
446
452
 
447
- # Step 1: Move Z to safe height (0) if XY movement is needed and Z is not already at 0
448
- if needs_xy_move and abs(current_z) > 0.001: # Small tolerance for floating point
449
- # Validate safe height (Z=0) is within limits
450
- self._validate_move_positions(z=0.0)
453
+ # Step 1: Move Z and A to SAFE_MOVE_HEIGHT if XY movement is needed
454
+ if needs_xy_move:
451
455
  self._logger.info(
452
- "Safe move: Raising Z to safe height (0) before XY movement"
456
+ "Safe move: Raising Z and A to safe height (%s) before XY movement", self.SAFE_MOVE_HEIGHT
453
457
  )
454
- move_cmd = f"G1 Z0 F{feed_rate}"
455
- self._send_command(self._build_command(move_cmd))
456
- self.current_position["Z"] = 0.0
457
- self._logger.debug("Z moved to safe height (0)")
458
-
459
- # Step 2: Move X, Y (and optionally A) to target
460
- if needs_xy_move or has_a:
458
+ move_cmd = f"G1 Z-5 A-5 F{self._z_feed}"
459
+ self.execute(self._build_command(move_cmd))
460
+ self.wait_for_move()
461
+ self._current_position["Z"] = self.SAFE_MOVE_HEIGHT
462
+ self._current_position["A"] = self.SAFE_MOVE_HEIGHT
463
+ self._logger.debug("Z and A moved to safe height (%s)", self.SAFE_MOVE_HEIGHT)
464
+
465
+ # Step 2: Move X, Y to target
466
+ if needs_xy_move:
461
467
  move_cmd = "G1"
462
- if has_x:
463
- move_cmd += f" X{target_pos['X']}"
464
- if has_y:
465
- move_cmd += f" Y{target_pos['Y']}"
466
- if has_a:
467
- move_cmd += f" A{target_pos['A']}"
468
- move_cmd += f" F{feed_rate}"
468
+ if needs_x_move:
469
+ move_cmd += f" X{position['X']}"
470
+ if needs_y_move:
471
+ move_cmd += f" Y{position['Y']}"
472
+ move_cmd += f" F{feed}"
469
473
 
470
474
  self._logger.info("Executing XY move command: %s", move_cmd)
471
- self._send_command(self._build_command(move_cmd))
475
+ self.execute(self._build_command(move_cmd))
476
+ self.wait_for_move()
472
477
 
473
478
  # Update position for moved axes
474
- if has_x:
475
- self.current_position["X"] = target_pos["X"]
476
- if has_y:
477
- self.current_position["Y"] = target_pos["Y"]
478
- if has_a:
479
- self.current_position["A"] = target_pos["A"]
480
-
481
- # Step 3: Move Z to target position (if Z movement was requested)
482
- if has_z:
483
- z_needs_move = abs(target_z - (0.0 if needs_xy_move else current_z)) > 0.001
484
- if z_needs_move:
485
- move_cmd = f"G1 Z{target_z} F{feed_rate}"
486
-
487
- if needs_xy_move:
488
- self._logger.info(
489
- "Safe move: Lowering Z to target position: %s", target_z
490
- )
491
- else:
492
- self._logger.info("Executing Z move command: %s", move_cmd)
493
-
494
- self._send_command(self._build_command(move_cmd))
495
- self.current_position["Z"] = target_z
479
+ if needs_x_move:
480
+ self._current_position["X"] = position['X']
481
+ if needs_y_move:
482
+ self._current_position["Y"] = position['Y']
483
+
484
+ # Step 3: Move Z and A back to original position (or target if specified)
485
+ if needs_z_move:
486
+ move_cmd = f"G1 Z{position['Z']} F{self._z_feed}"
487
+ self.execute(self._build_command(move_cmd))
488
+ self._current_position["Z"] = position['Z']
489
+ elif needs_a_move:
490
+ move_cmd = f"G1 A{position['A']} F{self._z_feed}"
491
+ self.execute(self._build_command(move_cmd))
492
+ self._current_position["A"] = position['A']
493
+ self.wait_for_move()
496
494
 
497
495
  self._logger.info(
498
- "Move complete. Final position: %s", self.current_position
496
+ "Move complete. Final position: %s", self._current_position
499
497
  )
500
- self._logger.debug("New internal position: %s", self.current_position)
498
+ self._logger.debug("New internal position: %s", self._current_position)
501
499
 
502
- # Post-move position synchronization check
500
+ # Step 4: Post-move position synchronization check
503
501
  self.sync_position()
502
+
503
+ return self._current_position
504
504
 
505
505
  def query_position(self) -> Dict[str, float]:
506
506
  """
@@ -555,8 +555,8 @@ class GCodeController(SerialController):
555
555
  queried_position = self.query_position()
556
556
 
557
557
  if not queried_position:
558
- self._logger.warning("Query position failed. Cannot synchronize.")
559
- return False, self.current_position
558
+ self._logger.error("Query position failed. Cannot synchronize.")
559
+ raise ValueError("Query position failed. Cannot synchronize.")
560
560
 
561
561
  # Compare internal vs. queried position
562
562
  axis_keys = ["X", "Y", "Z", "A"]
@@ -564,36 +564,31 @@ class GCodeController(SerialController):
564
564
 
565
565
  for axis in axis_keys:
566
566
  if (
567
- axis in self.current_position
567
+ axis in self._current_position
568
568
  and axis in queried_position
569
- and abs(self.current_position[axis] - queried_position[axis])
569
+ and abs(self._current_position[axis] - queried_position[axis])
570
570
  > self.TOLERANCE
571
571
  ):
572
572
  self._logger.warning(
573
573
  "Position mismatch found on %s axis: Internal=%.3f, Queried=%.3f",
574
574
  axis,
575
- self.current_position[axis],
575
+ self._current_position[axis],
576
576
  queried_position[axis],
577
577
  )
578
578
  adjustment_needed = True
579
579
  elif axis in queried_position:
580
580
  # Update internal position with queried position if it differs slightly
581
- self.current_position[axis] = queried_position[axis]
581
+ self._current_position[axis] = queried_position[axis]
582
582
 
583
583
  # Perform re-synchronization move if needed
584
584
  if adjustment_needed:
585
585
  self._logger.info(
586
- "** DISCREPANCY DETECTED. Moving robot back to internal position: %s **",
587
- self.current_position,
586
+ "** DISCREPANCY DETECTED. Moving robot to internal position: %s **",
587
+ self._current_position,
588
588
  )
589
589
 
590
590
  try:
591
- target_x = self.current_position.get("X")
592
- target_y = self.current_position.get("Y")
593
- target_z = self.current_position.get("Z")
594
- target_a = self.current_position.get("A")
595
-
596
- self.move_absolute(x=target_x, y=target_y, z=target_z, a=target_a)
591
+ self.move_absolute(x=self._current_position["X"], y=self._current_position["Y"], z=self._current_position["Z"], a=self._current_position["A"])
597
592
  self._logger.info("Synchronization move successfully completed.")
598
593
 
599
594
  # Recursive call to verify position after move
@@ -609,7 +604,7 @@ class GCodeController(SerialController):
609
604
  else:
610
605
  self._logger.info("No adjustment was made.")
611
606
 
612
- return adjustment_needed, self.current_position.copy()
607
+ return adjustment_needed, self._current_position.copy()
613
608
 
614
609
  def get_info(self) -> str:
615
610
  """
@@ -628,5 +623,5 @@ class GCodeController(SerialController):
628
623
  Returns:
629
624
  Dictionary containing the current internal position for all axes
630
625
  """
631
- self._logger.debug("Returning internal position: %s", self.current_position)
632
- return self.current_position.copy()
626
+ self._logger.debug("Returning internal position: %s", self._current_position)
627
+ return self._current_position.copy()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: puda-drivers
3
- Version: 0.0.4
3
+ Version: 0.0.5
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
@@ -56,13 +56,19 @@ from puda_drivers.move import GCodeController
56
56
  gantry = GCodeController(port_name="/dev/ttyACM0", feed=3000)
57
57
  gantry.connect()
58
58
 
59
+ # Configure axis limits for safety (recommended)
60
+ gantry.set_axis_limits("X", 0, 200)
61
+ gantry.set_axis_limits("Y", -200, 0)
62
+ gantry.set_axis_limits("Z", -100, 0)
63
+ gantry.set_axis_limits("A", -180, 180)
64
+
59
65
  # Home the gantry
60
66
  gantry.home()
61
67
 
62
- # Move to absolute position
68
+ # Move to absolute position (validated against limits)
63
69
  gantry.move_absolute(x=50.0, y=-100.0, z=-10.0)
64
70
 
65
- # Move relative to current position
71
+ # Move relative to current position (validated after conversion to absolute)
66
72
  gantry.move_relative(x=20.0, y=-10.0)
67
73
 
68
74
  # Query current position
@@ -73,6 +79,8 @@ print(f"Current position: {position}")
73
79
  gantry.disconnect()
74
80
  ```
75
81
 
82
+ **Axis Limits and Validation**: The `move_absolute()` and `move_relative()` methods automatically validate that target positions are within configured axis limits. If a position is outside the limits, a `ValueError` is raised before any movement is executed. Use `set_axis_limits()` to configure limits for each axis.
83
+
76
84
  ### Liquid Handling (Sartorius)
77
85
 
78
86
  ```python
@@ -134,6 +142,7 @@ pipette.disconnect()
134
142
  - Supports X, Y, Z, and A axes
135
143
  - Configurable feed rates
136
144
  - Position synchronization and homing
145
+ - Automatic axis limit validation for safe operation
137
146
 
138
147
  ### Liquid Handling
139
148
 
@@ -142,6 +151,38 @@ pipette.disconnect()
142
151
  - Tip attachment and ejection
143
152
  - Configurable speeds and volumes
144
153
 
154
+ ## Error Handling
155
+
156
+ ### Axis Limit Validation
157
+
158
+ Both `move_absolute()` and `move_relative()` validate positions against configured axis limits before executing any movement. If a position is outside the limits, a `ValueError` is raised:
159
+
160
+ ```python
161
+ from puda_drivers.move import GCodeController
162
+
163
+ gantry = GCodeController(port_name="/dev/ttyACM0")
164
+ gantry.connect()
165
+
166
+ # Set axis limits
167
+ gantry.set_axis_limits("X", 0, 200)
168
+ gantry.set_axis_limits("Y", -200, 0)
169
+
170
+ try:
171
+ # This will raise ValueError: Value 250 outside axis limits [0, 200]
172
+ gantry.move_absolute(x=250.0, y=-50.0)
173
+ except ValueError as e:
174
+ print(f"Move rejected: {e}")
175
+
176
+ # Relative moves are also validated after conversion to absolute positions
177
+ try:
178
+ # If current X is 150, moving 100 more would exceed the limit
179
+ gantry.move_relative(x=100.0)
180
+ except ValueError as e:
181
+ print(f"Move rejected: {e}")
182
+ ```
183
+
184
+ Validation errors are automatically logged at the ERROR level before the exception is raised.
185
+
145
186
  ## Finding Serial Ports
146
187
 
147
188
  To discover available serial ports on your system:
@@ -1,9 +1,9 @@
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=c-_kMLKxz9hLhpG-54wENkLeN0bNzY1rP9zML1RV9uc,7660
4
+ puda_drivers/core/serialcontroller.py,sha256=I7TsLNl45HPrO29LkhMRIQQ8fWdjdJUvIwQQ5swbMHM,7660
5
5
  puda_drivers/move/__init__.py,sha256=i7G5VKD5FgnmC21TLxoASVtC88IrPUTLDJrTnp99u-0,35
6
- puda_drivers/move/gcode.py,sha256=hgI5YzvSABJBlz5QlaNoUysB_7m9dhqXXkNm-zguYqQ,21504
6
+ puda_drivers/move/gcode.py,sha256=egZw3D5m9d1R8P32L1wd3lDwiWcFMDGPHsFMFIYXkRA,22069
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
@@ -11,7 +11,7 @@ puda_drivers/transfer/liquid/sartorius/__init__.py,sha256=QGpKz5YUwa8xCdSMXeZ0iR
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
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,,
14
+ puda_drivers-0.0.5.dist-info/METADATA,sha256=5VB_QiC_hcuV4-FEoXi-qEp637jAjQLiv5fipHX9QPg,6559
15
+ puda_drivers-0.0.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
16
+ puda_drivers-0.0.5.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
17
+ puda_drivers-0.0.5.dist-info/RECORD,,