puda-drivers 0.0.6__py3-none-any.whl → 0.0.7__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.
@@ -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
 
@@ -191,8 +191,15 @@ class SerialController(ABC):
191
191
  )
192
192
 
193
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
194
201
 
195
- def execute(self, command: str) -> str:
202
+ def execute(self, command: str, value: Optional[str] = None) -> str:
196
203
  """
197
204
  Send a command and read the response atomically.
198
205
 
@@ -211,5 +218,5 @@ class SerialController(ABC):
211
218
  serial.SerialException: If device is not connected or communication fails
212
219
  serial.SerialTimeoutException: If no response is received within timeout
213
220
  """
214
- self._send_command(command)
221
+ self._send_command(self._build_command(command, value))
215
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 wait_for_move(self) -> None:
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(self._build_command("M400"))
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(self._build_command(cmd))
309
- self._logger.info("Homing of %s completed.", home_target)
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(self._build_command("G90"))
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.info(
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(self._build_command(move_cmd))
460
- self.wait_for_move()
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.info("Executing XY move command: %s", move_cmd)
475
- self.execute(self._build_command(move_cmd))
476
- self.wait_for_move()
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(self._build_command(move_cmd))
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(self._build_command(move_cmd))
491
+ self.execute(move_cmd)
492
492
  self._current_position["A"] = position['A']
493
- self.wait_for_move()
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.sync_position()
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(self._build_command("M114"))
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 sync_position(self) -> Tuple[bool, Dict[str, float]]:
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.sync_position()
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(self._build_command("M115"))
617
+ return self.execute("M115")
618
618
 
619
619
  def get_internal_position(self) -> Dict[str, float]:
620
620
  """
@@ -72,7 +72,7 @@ class SartoriusController(SerialController):
72
72
  timeout,
73
73
  )
74
74
 
75
- def _build_command(self, command_code: str, value: str = "") -> str:
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
- + "R"
92
- + command_code
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._execute_command("Z", operation="Pipette initialization")
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._execute_command("DI", operation="Inward speed query")
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 **", speed)
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._execute_command("I", value=str(speed), operation="Setting inward speed")
225
- self._logger.info("** Inward Speed Set to %s Successfully **", speed)
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._execute_command("DO", operation="Outward speed query")
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 **", speed)
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._execute_command("O", value=str(speed), operation="Setting outward speed")
263
- self._logger.info("** Outward Speed Set to %s Successfully **", speed)
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._execute_command("P", value=position_str, operation="Run to position")
279
- self._logger.info("** Reached Position %s Successfully **", position)
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._execute_command("I", value=str(steps), operation="Aspirate")
298
- self._logger.info("** Aspirated %s uL Successfully **", amount)
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._execute_command("O", value=str(steps), operation="Dispense")
317
- self._logger.info("** Dispensed %s uL Successfully **", amount)
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._execute_command(
337
- "E", value=position_str, operation="Eject tip with return position"
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._execute_command(
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._execute_command("B", operation="Blowout")
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._execute_command("DS", operation="Status query")
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: puda-drivers
3
- Version: 0.0.6
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
@@ -2,17 +2,17 @@ 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=yYsOLXl32msFNRGrXLqhNVl_OfNPFR4ED7pmgn7rPU0,171
4
4
  puda_drivers/core/logging.py,sha256=prOeJ3CGEbm37TtMRyAOTQQiMU5_ImZTRXmcUJxkenc,2892
5
- puda_drivers/core/serialcontroller.py,sha256=9Vgz884MWmq4Z7rQzIQ7DtPzygMuzBeNWj0JiUpAhGA,7996
5
+ puda_drivers/core/serialcontroller.py,sha256=X8LZSda1LGUk7E3RpAb1G8Ox1F6k5sutU82bsUffSHA,8276
6
6
  puda_drivers/move/__init__.py,sha256=i7G5VKD5FgnmC21TLxoASVtC88IrPUTLDJrTnp99u-0,35
7
- puda_drivers/move/gcode.py,sha256=egZw3D5m9d1R8P32L1wd3lDwiWcFMDGPHsFMFIYXkRA,22069
7
+ puda_drivers/move/gcode.py,sha256=KBag8-lENIOZm54koCamP9BfgDEj1yV5Vchr3NcfC6w,22014
8
8
  puda_drivers/move/grbl/__init__.py,sha256=vBeeti8DVN2dACi1rLmHN_UGIOdo0s-HZX6mIepLV5I,98
9
9
  puda_drivers/move/grbl/api.py,sha256=loj8_Vap7S9qaD0ReHhgxr9Vkl6Wp7DGzyLkZyZ6v_k,16995
10
10
  puda_drivers/move/grbl/constants.py,sha256=4736CRDzLGWVqGscLajMlrIQMyubsHfthXi4RF1CHNg,9585
11
11
  puda_drivers/transfer/liquid/sartorius/__init__.py,sha256=QGpKz5YUwa8xCdSMXeZ0iRU-hRVqAWNPK0mlMTuzv8I,101
12
12
  puda_drivers/transfer/liquid/sartorius/api.py,sha256=jxwIJmY2k1K2ts6NC2ZgFTe4MOiH8TGnJeqYOqNa3rE,28250
13
13
  puda_drivers/transfer/liquid/sartorius/constants.py,sha256=mcsjLrVBH-RSodH-pszstwcEL9wwbV0vOgHbGNxZz9w,2770
14
- puda_drivers/transfer/liquid/sartorius/sartorius.py,sha256=iW3v-YHjj4ZAfGv0x0J-XV-Y0fAAhS6xmSg2ozQm4UI,13803
15
- puda_drivers-0.0.6.dist-info/METADATA,sha256=ELS1LZxhddh-dD-b0iKDrO5RnuJrrgITv6F6GeJ20wY,8598
16
- puda_drivers-0.0.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
17
- puda_drivers-0.0.6.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
18
- puda_drivers-0.0.6.dist-info/RECORD,,
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,,