puda-drivers 0.0.7__py3-none-any.whl → 0.0.8__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.
@@ -7,12 +7,14 @@ absolute coordinates, with relative moves converted to absolute internally.
7
7
  Supports homing and position synchronization.
8
8
  """
9
9
 
10
+ import time
10
11
  import re
11
12
  import logging
12
13
  from dataclasses import dataclass
13
14
  from typing import Optional, Dict, Tuple, Union
14
15
 
15
16
  from puda_drivers.core.serialcontroller import SerialController
17
+ from puda_drivers.core.position import Position
16
18
 
17
19
 
18
20
  @dataclass
@@ -90,12 +92,7 @@ class GCodeController(SerialController):
90
92
  )
91
93
 
92
94
  # Tracks internal position state
93
- self._current_position: Dict[str, float] = {
94
- "X": 0.0,
95
- "Y": 0.0,
96
- "Z": 0.0,
97
- "A": 0.0,
98
- }
95
+ self._current_position = Position(x=0.0, y=0.0, z=0.0, a=0.0)
99
96
  self._feed: int = feed
100
97
  self._z_feed: int = z_feed
101
98
 
@@ -191,51 +188,45 @@ class GCodeController(SerialController):
191
188
 
192
189
  def _validate_move_positions(
193
190
  self,
194
- x: Optional[float] = None,
195
- y: Optional[float] = None,
196
- z: Optional[float] = None,
197
- a: Optional[float] = None,
191
+ position: Position,
198
192
  ) -> None:
199
193
  """
200
194
  Validate that move positions are within axis limits.
201
195
 
202
- Only validates axes that are being moved (not None). Raises ValueError
196
+ Only validates axes that are present in the position. Raises ValueError
203
197
  if any position is outside the configured limits.
204
198
 
205
199
  Args:
206
- x: Target X position (optional)
207
- y: Target Y position (optional)
208
- z: Target Z position (optional)
209
- a: Target A position (optional)
200
+ position: Position object to validate
210
201
 
211
202
  Raises:
212
203
  ValueError: If any position is outside the axis limits
213
204
  """
214
- if x is not None:
205
+ if position.has_axis("x"):
215
206
  if "X" in self._axis_limits:
216
207
  try:
217
- self._axis_limits["X"].validate(x)
208
+ self._axis_limits["X"].validate(position.x)
218
209
  except ValueError as e:
219
210
  self._logger.error("Move validation failed for X axis: %s", e)
220
211
  raise
221
- if y is not None:
212
+ if position.has_axis("y"):
222
213
  if "Y" in self._axis_limits:
223
214
  try:
224
- self._axis_limits["Y"].validate(y)
215
+ self._axis_limits["Y"].validate(position.y)
225
216
  except ValueError as e:
226
217
  self._logger.error("Move validation failed for Y axis: %s", e)
227
218
  raise
228
- if z is not None:
219
+ if position.has_axis("z"):
229
220
  if "Z" in self._axis_limits:
230
221
  try:
231
- self._axis_limits["Z"].validate(z)
222
+ self._axis_limits["Z"].validate(position.z)
232
223
  except ValueError as e:
233
224
  self._logger.error("Move validation failed for Z axis: %s", e)
234
225
  raise
235
- if a is not None:
226
+ if position.has_axis("a"):
236
227
  if "A" in self._axis_limits:
237
228
  try:
238
- self._axis_limits["A"].validate(a)
229
+ self._axis_limits["A"].validate(position.a)
239
230
  except ValueError as e:
240
231
  self._logger.error("Move validation failed for A axis: %s", e)
241
232
  raise
@@ -310,10 +301,9 @@ class GCodeController(SerialController):
310
301
 
311
302
  # Update internal position (optimistic zeroing)
312
303
  if axis:
313
- self._current_position[axis] = 0.0
304
+ self._current_position[axis.lower()] = 0.0
314
305
  else:
315
- for key in self._current_position:
316
- self._current_position[key] = 0.0
306
+ self._current_position = Position(x=0.0, y=0.0, z=0.0, a=0.0)
317
307
 
318
308
  self._logger.debug(
319
309
  "Internal position updated (optimistically zeroed) to %s",
@@ -322,99 +312,118 @@ class GCodeController(SerialController):
322
312
 
323
313
  def move_absolute(
324
314
  self,
325
- x: Optional[float] = None,
326
- y: Optional[float] = None,
327
- z: Optional[float] = None,
328
- a: Optional[float] = None,
315
+ position: Position,
329
316
  feed: Optional[int] = None,
330
- ) -> Dict[str, float]:
317
+ ) -> Position:
331
318
  """
332
319
  Move to an absolute position (G90 + G1 command).
333
320
 
334
321
  Args:
335
- x: Target X position (optional)
336
- y: Target Y position (optional)
337
- z: Target Z position (optional)
338
- a: Target A position (optional)
322
+ position: Target Position object. Missing axes will use current position values.
339
323
  feed: Feed rate for this move (optional, uses current feed if not specified)
340
324
 
341
325
  Raises:
342
326
  ValueError: If any position is outside the axis limits
343
- """
344
- # Validate positions before executing move
345
- self._validate_move_positions(x=x, y=y, z=z, a=a)
346
327
 
328
+ Examples:
329
+ >>> pos = Position(x=10, y=20, z=30)
330
+ >>> controller.move_absolute(position=pos)
331
+ >>> # Partial position (only x and y specified)
332
+ >>> pos = Position(x=10, y=20)
333
+ >>> controller.move_absolute(position=pos) # z and a use current values
334
+ """
347
335
  # 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"]
336
+ final_x = position.x if position.has_axis("x") else self._current_position.x
337
+ final_y = position.y if position.has_axis("y") else self._current_position.y
338
+ final_z = position.z if position.has_axis("z") else self._current_position.z
339
+ final_a = position.a if position.has_axis("a") else self._current_position.a
340
+
341
+ # Create final position for validation and execution
342
+ final_position = Position(x=final_x, y=final_y, z=final_z, a=final_a)
343
+
344
+ # Validate positions before executing move
345
+ self._validate_move_positions(position=final_position)
352
346
 
353
347
  feed_rate = feed if feed is not None else self._feed
354
348
  self._logger.info(
355
349
  "Preparing absolute move to X:%s, Y:%s, Z:%s, A:%s at F:%s",
356
- target_x,
357
- target_y,
358
- target_z,
359
- target_a,
350
+ final_x,
351
+ final_y,
352
+ final_z,
353
+ final_a,
360
354
  feed_rate,
361
355
  )
362
356
 
363
357
  return self._execute_move(
364
- position={"X": target_x, "Y": target_y, "Z": target_z, "A": target_a},
358
+ position=final_position,
365
359
  feed=feed_rate
366
360
  )
367
361
 
368
362
  def move_relative(
369
363
  self,
370
- x: Optional[float] = None,
371
- y: Optional[float] = None,
372
- z: Optional[float] = None,
373
- a: Optional[float] = None,
364
+ position: Position,
374
365
  feed: Optional[int] = None,
375
- ) -> Dict[str, float]:
366
+ ) -> Position:
376
367
  """
377
368
  Move relative to the current position (converted to absolute move internally).
378
369
 
379
370
  Args:
380
- x: Relative X movement (optional)
381
- y: Relative Y movement (optional)
382
- z: Relative Z movement (optional)
383
- a: Relative A movement (optional)
371
+ position: Relative Position object. Only axes specified in position will move.
384
372
  feed: Feed rate for this move (optional, uses current feed if not specified)
385
373
 
386
374
  Raises:
387
375
  ValueError: If any resulting absolute position is outside the axis limits
388
- """
376
+
377
+ Examples:
378
+ >>> # Move relative in x and y only
379
+ >>> pos = Position(x=10, y=20)
380
+ >>> controller.move_relative(position=pos)
381
+ >>> # Move relative in all axes
382
+ >>> pos = Position(x=10, y=20, z=5, a=0)
383
+ >>> controller.move_relative(position=pos)
384
+ """
385
+ # Convert relative movements to absolute positions
386
+ # Only move axes that are specified in the position
387
+ abs_x = self._current_position.x
388
+ abs_y = self._current_position.y
389
+ abs_z = self._current_position.z
390
+ abs_a = self._current_position.a
391
+
392
+ if position.has_axis("x"):
393
+ abs_x = self._current_position.x + position.x
394
+ if position.has_axis("y"):
395
+ abs_y = self._current_position.y + position.y
396
+ if position.has_axis("z"):
397
+ abs_z = self._current_position.z + position.z
398
+ if position.has_axis("a"):
399
+ abs_a = self._current_position.a + position.a
400
+
401
+ # Create absolute position for validation and execution
402
+ abs_position = Position(x=abs_x, y=abs_y, z=abs_z, a=abs_a)
403
+
404
+ # Validate absolute positions before executing move
405
+ self._validate_move_positions(position=abs_position)
406
+
389
407
  feed_rate = feed if feed is not None else self._feed
390
408
  self._logger.info(
391
409
  "Preparing relative move by dX:%s, dY:%s, dZ:%s, dA:%s at F:%s",
392
- x,
393
- y,
394
- z,
395
- a,
410
+ position.x if position.has_axis("x") else 0,
411
+ position.y if position.has_axis("y") else 0,
412
+ position.z if position.has_axis("z") else 0,
413
+ position.a if position.has_axis("a") else 0,
396
414
  feed_rate,
397
415
  )
398
416
 
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"]
404
-
405
- # Validate absolute positions before executing move
406
- self._validate_move_positions(x=abs_x, y=abs_y, z=abs_z, a=abs_a)
407
-
408
417
  return self._execute_move(
409
- position={"X": abs_x, "Y": abs_y, "Z": abs_z, "A": abs_a},
418
+ position=abs_position,
410
419
  feed=feed_rate
411
420
  )
412
421
 
413
422
  def _execute_move(
414
423
  self,
415
- position: Dict[str, float],
424
+ position: Position,
416
425
  feed: int,
417
- ) -> Dict[str, float]:
426
+ ) -> Position:
418
427
  """
419
428
  Internal helper for executing G1 move commands with safe movement pattern.
420
429
  All coordinates are treated as absolute positions.
@@ -425,15 +434,15 @@ class GCodeController(SerialController):
425
434
  3. Finally move Z and A back to original position (or target if specified)
426
435
 
427
436
  Args:
428
- position: Dictionary with absolute positions for X, Y, Z, A axes
437
+ position: Position with absolute positions for X, Y, Z, A axes
429
438
  feed: Feed rate for the move
430
439
  """
431
440
  # 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
-
441
+ needs_x_move = abs(position.x - self._current_position.x) > self.TOLERANCE
442
+ needs_y_move = abs(position.y - self._current_position.y) > self.TOLERANCE
443
+ needs_z_move = abs(position.z - self._current_position.z) > self.TOLERANCE
444
+ needs_a_move = abs(position.a - self._current_position.a) > self.TOLERANCE
445
+
437
446
  if not (needs_x_move or needs_y_move or needs_z_move or needs_a_move):
438
447
  self._logger.warning(
439
448
  "Move command issued without any axis movement. Skipping transmission."
@@ -448,27 +457,30 @@ class GCodeController(SerialController):
448
457
 
449
458
  # Step 0: Ensure absolute mode is active
450
459
  self.execute("G90")
451
- needs_xy_move = needs_x_move or needs_y_move
452
460
 
453
461
  # Step 1: Move Z and A to SAFE_MOVE_HEIGHT if XY movement is needed
454
- if needs_xy_move:
462
+ if needs_x_move or needs_y_move:
455
463
  self._logger.debug(
456
464
  "Safe move: Raising Z and A to safe height (%s) before XY movement", self.SAFE_MOVE_HEIGHT
457
465
  )
458
466
  move_cmd = f"G1 Z-5 A-5 F{self._z_feed}"
459
467
  self.execute(move_cmd)
460
468
  self._wait_for_move()
461
- self._current_position["Z"] = self.SAFE_MOVE_HEIGHT
462
- self._current_position["A"] = self.SAFE_MOVE_HEIGHT
469
+ self._current_position.z = self.SAFE_MOVE_HEIGHT
470
+ self._current_position.a = self.SAFE_MOVE_HEIGHT
463
471
  self._logger.debug("Z and A moved to safe height (%s)", self.SAFE_MOVE_HEIGHT)
464
472
 
473
+ # update needs_z_move and needs_a_move based on the current position
474
+ needs_z_move = abs(position.z - self._current_position.z) > self.TOLERANCE
475
+ needs_a_move = abs(position.a - self._current_position.a) > self.TOLERANCE
476
+
465
477
  # Step 2: Move X, Y to target
466
- if needs_xy_move:
478
+ if needs_x_move or needs_y_move:
467
479
  move_cmd = "G1"
468
480
  if needs_x_move:
469
- move_cmd += f" X{position['X']}"
481
+ move_cmd += f" X{position.x}"
470
482
  if needs_y_move:
471
- move_cmd += f" Y{position['Y']}"
483
+ move_cmd += f" Y{position.y}"
472
484
  move_cmd += f" F{feed}"
473
485
 
474
486
  self._logger.debug("Executing XY move command: %s", move_cmd)
@@ -477,39 +489,34 @@ class GCodeController(SerialController):
477
489
 
478
490
  # Update position for moved axes
479
491
  if needs_x_move:
480
- self._current_position["X"] = position['X']
492
+ self._current_position.x = position.x
481
493
  if needs_y_move:
482
- self._current_position["Y"] = position['Y']
494
+ self._current_position.y = position.y
483
495
 
484
496
  # Step 3: Move Z and A back to original position (or target if specified)
485
497
  if needs_z_move:
486
- move_cmd = f"G1 Z{position['Z']} F{self._z_feed}"
498
+ move_cmd = f"G1 Z{position.z} F{self._z_feed}"
487
499
  self.execute(move_cmd)
488
- self._current_position["Z"] = position['Z']
500
+ self._wait_for_move()
501
+ self._current_position.z = position.z
489
502
  elif needs_a_move:
490
- move_cmd = f"G1 A{position['A']} F{self._z_feed}"
503
+ move_cmd = f"G1 A{position.a} F{self._z_feed}"
491
504
  self.execute(move_cmd)
492
- self._current_position["A"] = position['A']
493
- self._wait_for_move()
505
+ self._wait_for_move()
506
+ self._current_position.a = position.a
494
507
  self._logger.debug("New internal position: %s", self._current_position)
495
-
496
- # Step 4: Post-move position synchronization check
497
- self._sync_position()
498
- self._logger.info(
499
- "Move complete. Final position: %s\n", self._current_position
500
- )
501
508
 
502
509
  return self._current_position
503
510
 
504
- def query_position(self) -> Dict[str, float]:
511
+ def query_position(self) -> Position:
505
512
  """
506
513
  Query the current machine position (M114 command).
507
514
 
508
515
  Returns:
509
- Dictionary containing X, Y, Z, and A positions
516
+ Position containing X, Y, Z, and A positions
510
517
 
511
518
  Note:
512
- Returns an empty dictionary if the query fails or no positions are found.
519
+ Returns an empty Position if the query fails or no positions are found.
513
520
  """
514
521
  self._logger.info("Querying current machine position (M114).")
515
522
  res: str = self.execute("M114")
@@ -522,7 +529,7 @@ class GCodeController(SerialController):
522
529
 
523
530
  for axis, value_str in matches:
524
531
  try:
525
- position_data[axis] = float(value_str)
532
+ position_data[axis.lower()] = float(value_str)
526
533
  except ValueError:
527
534
  self._logger.error(
528
535
  "Failed to convert position value '%s' for axis %s to float.",
@@ -531,10 +538,11 @@ class GCodeController(SerialController):
531
538
  )
532
539
  continue
533
540
 
534
- self._logger.info("Query position complete. Retrieved positions: %s", position_data)
535
- return position_data
541
+ position = Position.from_dict(position_data)
542
+ self._logger.info("Query position complete. Retrieved positions: %s", position)
543
+ return position
536
544
 
537
- def _sync_position(self) -> Tuple[bool, Dict[str, float]]:
545
+ def _sync_position(self) -> Tuple[bool, Position]:
538
546
  """
539
547
  Synchronize internal position with actual machine position.
540
548
 
@@ -543,7 +551,7 @@ class GCodeController(SerialController):
543
551
  it by moving to the internal position.
544
552
 
545
553
  Returns:
546
- Tuple of (adjustment_occurred: bool, final_position: Dict[str, float])
554
+ Tuple of (adjustment_occurred: bool, final_position: Position)
547
555
  where adjustment_occurred is True if a correction move was made.
548
556
 
549
557
  Note:
@@ -554,29 +562,30 @@ class GCodeController(SerialController):
554
562
  # Query the actual machine position
555
563
  queried_position = self.query_position()
556
564
 
557
- if not queried_position:
565
+ if not queried_position.get_axes():
558
566
  self._logger.error("Query position failed. Cannot synchronize.")
559
567
  raise ValueError("Query position failed. Cannot synchronize.")
560
568
 
561
569
  # Compare internal vs. queried position
562
- axis_keys = ["X", "Y", "Z", "A"]
570
+ axis_keys = ["x", "y", "z", "a"]
563
571
  adjustment_needed = False
564
572
 
565
573
  for axis in axis_keys:
574
+ axis_upper = axis.upper()
566
575
  if (
567
- axis in self._current_position
568
- and axis in queried_position
576
+ self._current_position.has_axis(axis)
577
+ and queried_position.has_axis(axis)
569
578
  and abs(self._current_position[axis] - queried_position[axis])
570
579
  > self.TOLERANCE
571
580
  ):
572
581
  self._logger.warning(
573
582
  "Position mismatch found on %s axis: Internal=%.3f, Queried=%.3f",
574
- axis,
583
+ axis_upper,
575
584
  self._current_position[axis],
576
585
  queried_position[axis],
577
586
  )
578
587
  adjustment_needed = True
579
- elif axis in queried_position:
588
+ elif queried_position.has_axis(axis):
580
589
  # Update internal position with queried position if it differs slightly
581
590
  self._current_position[axis] = queried_position[axis]
582
591
 
@@ -588,7 +597,7 @@ class GCodeController(SerialController):
588
597
  )
589
598
 
590
599
  try:
591
- self.move_absolute(x=self._current_position["X"], y=self._current_position["Y"], z=self._current_position["Z"], a=self._current_position["A"])
600
+ self.move_absolute(position=self._current_position.copy())
592
601
  self._logger.info("Synchronization move successfully completed.")
593
602
 
594
603
  # Recursive call to verify position after move
@@ -616,12 +625,12 @@ class GCodeController(SerialController):
616
625
  self._logger.info("Querying machine information (M115).")
617
626
  return self.execute("M115")
618
627
 
619
- def get_internal_position(self) -> Dict[str, float]:
628
+ def get_internal_position(self) -> Position:
620
629
  """
621
630
  Get the internally tracked position.
622
631
 
623
632
  Returns:
624
- Dictionary containing the current internal position for all axes
633
+ Position containing the current internal position for all axes
625
634
  """
626
635
  self._logger.debug("Returning internal position: %s", self._current_position)
627
- return self._current_position.copy()
636
+ return self._current_position.copy()
@@ -9,15 +9,13 @@ Reference: https://api.sartorius.com/document-hub/dam/download/34901/Sartorius-r
9
9
 
10
10
  import logging
11
11
  from typing import Optional
12
-
13
12
  from puda_drivers.core.serialcontroller import SerialController
14
-
15
13
  from .constants import STATUS_CODES
16
14
 
17
-
18
15
  class SartoriusDeviceError(Exception):
19
16
  """Custom exception raised when the Sartorius device reports an error."""
20
-
17
+
18
+ pass
21
19
 
22
20
  class SartoriusController(SerialController):
23
21
  """
@@ -71,6 +69,8 @@ class SartoriusController(SerialController):
71
69
  baudrate,
72
70
  timeout,
73
71
  )
72
+ self._tip_attached: bool
73
+ self._volume: int = 0
74
74
 
75
75
  def _build_command(self, command: str, value: Optional[str] = None) -> str:
76
76
  """
@@ -145,6 +145,7 @@ class SartoriusController(SerialController):
145
145
  self._logger.info("** Initializing Pipette Head (RZ) **")
146
146
  self.execute(command="RZ")
147
147
  self._logger.info("** Pipette Initialization Complete **\n")
148
+ self.set_tip_attached(attached=False)
148
149
 
149
150
  def get_inward_speed(self) -> int:
150
151
  """
@@ -239,7 +240,7 @@ class SartoriusController(SerialController):
239
240
  self._logger.info("** Reached Position %s Successfully **\n", position)
240
241
 
241
242
  # instead of run_inward, use aspirate
242
- def aspirate(self, amount: float) -> None:
243
+ def aspirate(self, amount: int) -> None:
243
244
  """
244
245
  Aspirate fluid from the current location.
245
246
 
@@ -257,9 +258,10 @@ class SartoriusController(SerialController):
257
258
  self._logger.info("** Aspirating %s uL (RI%s steps) **", amount, steps)
258
259
  self.execute(command="RI", value=str(steps))
259
260
  self._logger.info("** Aspirated %s uL Successfully **\n", amount)
261
+ self._volume += amount
260
262
 
261
263
  # instead of run_outward, use dispense
262
- def dispense(self, amount: float) -> None:
264
+ def dispense(self, amount: int) -> None:
263
265
  """
264
266
  Dispense fluid at the current location.
265
267
 
@@ -277,6 +279,7 @@ class SartoriusController(SerialController):
277
279
  self._logger.info("** Dispensing %s uL (RO%s steps) **", amount, steps)
278
280
  self.execute(command="RO", value=str(steps))
279
281
  self._logger.info("** Dispensed %s uL Successfully **\n", amount)
282
+ self._volume -= amount
280
283
 
281
284
  def eject_tip(self, return_position: int = 30) -> None:
282
285
  """
@@ -289,7 +292,10 @@ class SartoriusController(SerialController):
289
292
  ValueError: If return_position has leading zeros
290
293
  SartoriusDeviceError: If tip ejection fails
291
294
  """
292
- position_str = self._validate_no_leading_zeros(return_position, "RE")
295
+ position_str = self._validate_no_leading_zeros(
296
+ command_name="RE",
297
+ value=return_position
298
+ )
293
299
  self._logger.info(
294
300
  "** Ejecting Tip and returning to position %s (RE %s) **",
295
301
  return_position,
@@ -381,3 +387,21 @@ class SartoriusController(SerialController):
381
387
  response = self.execute(command="DN")
382
388
  self._logger.info("** Liquid Level: %s uL **\n", response)
383
389
  return response
390
+
391
+ def is_tip_attached(self) -> bool:
392
+ """
393
+ Check if a tip is attached to the pipette (DS command).
394
+
395
+ Returns:
396
+ True if a tip is attached, False otherwise
397
+ """
398
+ return self._tip_attached
399
+
400
+ def set_tip_attached(self, attached: bool) -> None:
401
+ """
402
+ Set the tip attached state (DS command).
403
+
404
+ Args:
405
+ attached: True if a tip is attached, False otherwise
406
+ """
407
+ self._tip_attached = attached
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: puda-drivers
3
- Version: 0.0.7
3
+ Version: 0.0.8
4
4
  Summary: Hardware drivers for the PUDA platform.
5
5
  Project-URL: Homepage, https://github.com/zhao-bears/puda-drivers
6
6
  Project-URL: Issues, https://github.com/zhao-bears/puda-drivers/issues
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Programming Language :: Python :: 3.14
15
15
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
16
  Classifier: Topic :: System :: Hardware
17
- Requires-Python: >=3.14
17
+ Requires-Python: >=3.8
18
18
  Requires-Dist: pyserial~=3.5
19
19
  Description-Content-Type: text/markdown
20
20
 
@@ -0,0 +1,27 @@
1
+ puda_drivers/__init__.py,sha256=rcF5xCkMgyLlJLN3gWwJnUoW0ShPyISeyENvaqwg4Ik,503
2
+ puda_drivers/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ puda_drivers/core/__init__.py,sha256=XbCdXsU6NMDsmEAtavAGiSZZPla5d7zc2L7Qx9qKHdY,214
4
+ puda_drivers/core/logging.py,sha256=prOeJ3CGEbm37TtMRyAOTQQiMU5_ImZTRXmcUJxkenc,2892
5
+ puda_drivers/core/position.py,sha256=f4efmDSrKKCtqrR-GUJxVitPG20MiuGSDOWt-9TVISk,12628
6
+ puda_drivers/core/serialcontroller.py,sha256=38mKas1iJaOkAE0_V4tmqgZz7RxMMEWfGqA0Ma_Dt2A,8604
7
+ puda_drivers/labware/__init__.py,sha256=RlRxrJn2zyzyxv4c1KGt8Gmxv2cRO8V4zZVnnyL-I00,288
8
+ puda_drivers/labware/labware.py,sha256=hZhOzSyb1GP_bm1LvQsREx4YSqLCWBTKNWDgDqfFavI,5317
9
+ puda_drivers/labware/opentrons_96_tiprack_300ul.json,sha256=jmNaworu688GEgFdxMxNRSaEp4ZOg9IumFk8bVHSzYY,19796
10
+ puda_drivers/labware/polyelectric_8_wellplate_30000ul.json,sha256=NwXMgHBYnIIFasmrthTDTTV7n1M5DYQBDWh-KYsq1gI,3104
11
+ puda_drivers/labware/trash_bin.json,sha256=X4TDNDzGbCtJSWlYgGYHUzdnVKj62SggGDNf7z5S0OE,581
12
+ puda_drivers/machines/__init__.py,sha256=zmIk_r2T8nbPA68h3Cko8N6oL7ncoBpmvhNcAqzHmc4,45
13
+ puda_drivers/machines/first.py,sha256=McnZ4q_ykv56aWFqphoZFqXqbhDGZAWCk40CPblgPsw,8492
14
+ puda_drivers/move/__init__.py,sha256=NKIKckcqgyviPM0EGFcmIoaqkJM4qekR4babfdddRzM,96
15
+ puda_drivers/move/deck.py,sha256=yq2B4WMqj0hQvHt8HoJskP10u1DUyKwUnjP2c9gJ174,1397
16
+ puda_drivers/move/gcode.py,sha256=Aw7la4RkPw737hW5sKl6WiPCmmTnsjLvG4mOb-RwVSc,22592
17
+ puda_drivers/move/grbl/__init__.py,sha256=vBeeti8DVN2dACi1rLmHN_UGIOdo0s-HZX6mIepLV5I,98
18
+ puda_drivers/move/grbl/api.py,sha256=loj8_Vap7S9qaD0ReHhgxr9Vkl6Wp7DGzyLkZyZ6v_k,16995
19
+ puda_drivers/move/grbl/constants.py,sha256=4736CRDzLGWVqGscLajMlrIQMyubsHfthXi4RF1CHNg,9585
20
+ puda_drivers/transfer/liquid/sartorius/__init__.py,sha256=QGpKz5YUwa8xCdSMXeZ0iRU-hRVqAWNPK0mlMTuzv8I,101
21
+ puda_drivers/transfer/liquid/sartorius/api.py,sha256=jxwIJmY2k1K2ts6NC2ZgFTe4MOiH8TGnJeqYOqNa3rE,28250
22
+ puda_drivers/transfer/liquid/sartorius/constants.py,sha256=mcsjLrVBH-RSodH-pszstwcEL9wwbV0vOgHbGNxZz9w,2770
23
+ puda_drivers/transfer/liquid/sartorius/sartorius.py,sha256=bW838jYOAfLlbUqtsKRipZ-RLjrNTcZ7riYV6I4w8G8,13728
24
+ puda_drivers-0.0.8.dist-info/METADATA,sha256=_dQYo29lMIclzyd3HGZBvmOidliS92UWTQPPO0g79V0,8597
25
+ puda_drivers-0.0.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
26
+ puda_drivers-0.0.8.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
27
+ puda_drivers-0.0.8.dist-info/RECORD,,
@@ -1,18 +0,0 @@
1
- puda_drivers/__init__.py,sha256=rcF5xCkMgyLlJLN3gWwJnUoW0ShPyISeyENvaqwg4Ik,503
2
- puda_drivers/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- puda_drivers/core/__init__.py,sha256=yYsOLXl32msFNRGrXLqhNVl_OfNPFR4ED7pmgn7rPU0,171
4
- puda_drivers/core/logging.py,sha256=prOeJ3CGEbm37TtMRyAOTQQiMU5_ImZTRXmcUJxkenc,2892
5
- puda_drivers/core/serialcontroller.py,sha256=X8LZSda1LGUk7E3RpAb1G8Ox1F6k5sutU82bsUffSHA,8276
6
- puda_drivers/move/__init__.py,sha256=i7G5VKD5FgnmC21TLxoASVtC88IrPUTLDJrTnp99u-0,35
7
- puda_drivers/move/gcode.py,sha256=KBag8-lENIOZm54koCamP9BfgDEj1yV5Vchr3NcfC6w,22014
8
- puda_drivers/move/grbl/__init__.py,sha256=vBeeti8DVN2dACi1rLmHN_UGIOdo0s-HZX6mIepLV5I,98
9
- puda_drivers/move/grbl/api.py,sha256=loj8_Vap7S9qaD0ReHhgxr9Vkl6Wp7DGzyLkZyZ6v_k,16995
10
- puda_drivers/move/grbl/constants.py,sha256=4736CRDzLGWVqGscLajMlrIQMyubsHfthXi4RF1CHNg,9585
11
- puda_drivers/transfer/liquid/sartorius/__init__.py,sha256=QGpKz5YUwa8xCdSMXeZ0iRU-hRVqAWNPK0mlMTuzv8I,101
12
- puda_drivers/transfer/liquid/sartorius/api.py,sha256=jxwIJmY2k1K2ts6NC2ZgFTe4MOiH8TGnJeqYOqNa3rE,28250
13
- puda_drivers/transfer/liquid/sartorius/constants.py,sha256=mcsjLrVBH-RSodH-pszstwcEL9wwbV0vOgHbGNxZz9w,2770
14
- puda_drivers/transfer/liquid/sartorius/sartorius.py,sha256=RM4S8AkHQ2JJEsYbw83I0nFJUT7BYHRa2y6vgdNQXyc,13008
15
- puda_drivers-0.0.7.dist-info/METADATA,sha256=93De176TfLUIk3-H8mM5LwLXGOCKMguxT-0rg6mozjc,8598
16
- puda_drivers-0.0.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
- puda_drivers-0.0.7.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
18
- puda_drivers-0.0.7.dist-info/RECORD,,