puda-drivers 0.0.15__py3-none-any.whl → 0.0.17__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.
@@ -4,6 +4,7 @@ Generic Serial Controller for communicating with devices over serial ports.
4
4
 
5
5
  import time
6
6
  import logging
7
+ import threading
7
8
  from typing import Optional, List, Tuple
8
9
  from abc import ABC, abstractmethod
9
10
  import serial
@@ -47,8 +48,11 @@ class SerialController(ABC):
47
48
  self._serial = None
48
49
  self.port_name = port_name
49
50
  self.baudrate = baudrate
50
- self.timeout = timeout
51
+ self._timeout = timeout
51
52
  self._logger = logger
53
+
54
+ # lock to prevent concurrent access to the serial port
55
+ self._lock = threading.Lock()
52
56
 
53
57
  def connect(self) -> None:
54
58
  """
@@ -71,7 +75,7 @@ class SerialController(ABC):
71
75
  self._serial = serial.Serial(
72
76
  port=self.port_name,
73
77
  baudrate=self.baudrate,
74
- timeout=self.timeout,
78
+ timeout=self._timeout,
75
79
  )
76
80
  self._serial.flush()
77
81
  self._logger.info("Successfully connected to %s.", self.port_name)
@@ -103,20 +107,20 @@ class SerialController(ABC):
103
107
 
104
108
  def _send_command(self, command: str) -> None:
105
109
  """
106
- Sends a custom protocol command, reads, and returns the response.
107
- If a timeout occurs, it logs the error and returns None instead of raising an exception.
110
+ Sends a command to the device.
111
+ Note: This method should be called while holding self._lock to ensure
112
+ atomic command/response pairing.
108
113
  """
114
+ self._logger.info("-> Sending: %r", command)
115
+
109
116
  if not self.is_connected or not self._serial:
110
117
  self._logger.error(
111
118
  "Attempt to send command '%s' failed: Device not connected.",
112
119
  command,
113
120
  )
114
121
  # Retain raising an error for being disconnected, as that's a connection state issue
115
- raise serial.SerialException("Device not connected. Call connect() first.")
122
+ raise serial.SerialException("Device disconnected. Call connect() first.")
116
123
 
117
- self._logger.info("-> Sending: %r", command)
118
-
119
- # Send the command
120
124
  try:
121
125
  self._serial.reset_input_buffer() # clear input buffer
122
126
  self._serial.reset_output_buffer() # clear output buffer
@@ -130,13 +134,13 @@ class SerialController(ABC):
130
134
 
131
135
  except serial.SerialException as e:
132
136
  self._logger.error(
133
- "Serial error writing or reading command '%s'. Error: %s",
137
+ "Serial error writing command '%s'. Error: %s",
134
138
  command,
135
139
  e,
136
140
  )
137
141
  return None
138
142
 
139
- def _read_response(self) -> str:
143
+ def _read_response(self, timeout: int = None) -> str:
140
144
  """
141
145
  Generic, blocking read that respects timeout and returns
142
146
  all data that arrived within the timeout period.
@@ -144,10 +148,13 @@ class SerialController(ABC):
144
148
  if not self.is_connected or not self._serial:
145
149
  raise serial.SerialException("Device not connected.")
146
150
 
151
+ if timeout is None:
152
+ timeout = self._timeout
153
+
147
154
  start_time = time.time()
148
155
  response = b""
149
156
 
150
- while time.time() - start_time < self.timeout:
157
+ while time.time() - start_time < timeout:
151
158
  if self._serial.in_waiting > 0:
152
159
  # Read all available bytes
153
160
  response += self._serial.read(self._serial.in_waiting)
@@ -164,14 +171,12 @@ class SerialController(ABC):
164
171
 
165
172
  # Timeout reached - check what we got
166
173
  if not response:
167
- self._logger.error("No response within %s seconds.", self.timeout)
174
+ self._logger.error("No response within %s seconds.", timeout)
168
175
  raise serial.SerialTimeoutException(
169
- f"No response received within {self.timeout} seconds."
176
+ f"No response received within {timeout} seconds."
170
177
  )
171
178
 
172
- # Decode once and check the decoded string
173
179
  decoded_response = response.decode("utf-8", errors="ignore").strip()
174
-
175
180
  if "ok" in decoded_response.lower():
176
181
  self._logger.debug("<- Received response: %r", decoded_response)
177
182
  elif "err" in decoded_response.lower():
@@ -189,20 +194,25 @@ class SerialController(ABC):
189
194
  def _build_command(self, command: str, value: Optional[str] = None) -> str:
190
195
  """
191
196
  Build a command string according to the device protocol.
197
+
198
+ There might be special starting and ending characters for devices
192
199
  """
193
- pass
200
+ raise NotImplementedError
194
201
 
195
- def execute(self, command: str, value: Optional[str] = None) -> str:
202
+ def execute(self, command: str, value: Optional[str] = None, timeout: int = None) -> str:
196
203
  """
197
204
  Send a command and read the response atomically.
198
205
 
199
206
  This method combines sending a command and reading its response into a
200
207
  single atomic operation, ensuring the response corresponds to the command
201
- that was just sent. This is the preferred method for commands that
202
- require a response.
208
+ that was just sent. The entire operation is protected by a lock to prevent
209
+ concurrent commands from interfering with each other.
210
+
211
+ This is the preferred method for commands that require a response.
203
212
 
204
213
  Args:
205
214
  command: Command string to send (should include protocol terminator if needed)
215
+ value: Optional value parameter for the command
206
216
 
207
217
  Returns:
208
218
  Response string from the device
@@ -211,20 +221,10 @@ class SerialController(ABC):
211
221
  serial.SerialException: If device is not connected or communication fails
212
222
  serial.SerialTimeoutException: If no response is received within timeout
213
223
  """
214
- self._send_command(self._build_command(command, value))
215
-
216
- # Increase timeout by 60 seconds for G28 (homing) command
217
- original_timeout = self.timeout
218
- if "G28" in command.upper():
219
- self.timeout = original_timeout + 60
220
- # Also update the serial connection's timeout if connected
221
- if self.is_connected and self._serial:
222
- self._serial.timeout = self.timeout
223
-
224
- try:
225
- return self._read_response()
226
- finally:
227
- # Restore original timeout
228
- self.timeout = original_timeout
229
- if self.is_connected and self._serial:
230
- self._serial.timeout = original_timeout
224
+ if timeout is None:
225
+ timeout = self._timeout
226
+ # Hold the lock for the entire send+read operation to ensure atomicity
227
+ # This prevents concurrent commands from mixing up responses
228
+ with self._lock:
229
+ self._send_command(self._build_command(command, value))
230
+ return self._read_response(timeout=timeout)
@@ -20,7 +20,8 @@
20
20
  "width": 10.0,
21
21
  "height": 10.0,
22
22
  "x": 60.0,
23
- "y": 42.0
23
+ "y": 42.0,
24
+ "z": 0.0
24
25
  }
25
26
  },
26
27
  "groups": [
@@ -159,7 +159,7 @@ class First:
159
159
  self.camera.disconnect()
160
160
  self._logger.info("Machine shutdown complete")
161
161
 
162
- async def get_position(self) -> Dict[str, float]:
162
+ async def get_position(self) -> Dict[str, Union[Dict[str, float], int]]:
163
163
  """
164
164
  Get the current position of the machine. Both QuBot and Sartorius are queried.
165
165
 
@@ -176,6 +176,8 @@ class First:
176
176
  "pipette": sartorius_position,
177
177
  }
178
178
 
179
+ ### Labware management ###
180
+
179
181
  def load_labware(self, slot: str, labware_name: str):
180
182
  """
181
183
  Load a labware object into a slot.
@@ -190,7 +192,33 @@ class First:
190
192
  self._logger.info("Loading labware '%s' into slot '%s'", labware_name, slot)
191
193
  self.deck.load_labware(slot=slot, labware_name=labware_name)
192
194
  self._logger.debug("Labware '%s' loaded into slot '%s'", labware_name, slot)
195
+
196
+ def remove_labware(self, slot: str):
197
+ """
198
+ Remove labware from a slot.
199
+
200
+ Args:
201
+ slot: Slot name (e.g., 'A1', 'B2')
202
+
203
+ Raises:
204
+ KeyError: If slot is not found in deck
205
+ """
206
+ self.deck.empty_slot(slot=slot)
207
+ self._logger.debug("Slot '%s' emptied", slot)
193
208
 
209
+ def get_deck(self):
210
+ """
211
+ Get the current deck layout.
212
+
213
+ Returns:
214
+ Dictionary mapping slot names (e.g., "A1") to labware classes.
215
+
216
+ Raises:
217
+ None
218
+ """
219
+ return self.deck.to_dict()
220
+
221
+
194
222
  def load_deck(self, deck_layout: Dict[str, Type[StandardLabware]]):
195
223
  """
196
224
  Load multiple labware into the deck at once.
@@ -211,6 +239,8 @@ class First:
211
239
  self.load_labware(slot=slot, labware_name=labware_name)
212
240
  self._logger.info("Deck layout loaded successfully")
213
241
 
242
+ ### Liquid handling ###
243
+
214
244
  def attach_tip(self, slot: str, well: Optional[str] = None):
215
245
  """
216
246
  Attach a tip from a slot.
@@ -234,7 +264,11 @@ class First:
234
264
  self.qubot.move_absolute(position=pos)
235
265
 
236
266
  # attach tip (move slowly down)
237
- insert_depth = self.deck[slot].get_insert_depth()
267
+ labware = self.deck[slot]
268
+ if labware is None:
269
+ self._logger.error("Cannot attach tip: no labware loaded in slot '%s'", slot)
270
+ raise ValueError(f"No labware loaded in slot '{slot}'. Load labware before attaching tips.")
271
+ insert_depth = labware.get_insert_depth()
238
272
  self._logger.debug("Moving down by %s mm to insert tip", insert_depth)
239
273
  self.qubot.move_relative(
240
274
  position=Position(z=-insert_depth),
@@ -378,17 +412,24 @@ class First:
378
412
 
379
413
  Returns:
380
414
  Position with absolute coordinates
415
+
416
+ Raises:
417
+ ValueError: If well is specified but no labware is loaded in the slot
381
418
  """
382
419
  # Get slot origin
383
420
  pos = self.get_slot_origin(slot)
384
421
 
385
422
  # relative well position from slot origin
386
423
  if well:
387
- well_pos = self.deck[slot].get_well_position(well).get_xy()
424
+ labware = self.deck[slot]
425
+ if labware is None:
426
+ self._logger.error("Cannot get well position: no labware loaded in slot '%s'", slot)
427
+ raise ValueError(f"No labware loaded in slot '{slot}'. Load labware before accessing wells.")
428
+ well_pos = labware.get_well_position(well).get_xy()
388
429
  # the deck is rotated 90 degrees clockwise for this machine
389
430
  pos += well_pos.swap_xy()
390
431
  # get z
391
- pos += Position(z=self.deck[slot].get_height() - self.CEILING_HEIGHT)
432
+ pos += Position(z=labware.get_height() - self.CEILING_HEIGHT)
392
433
  self._logger.debug("Absolute Z position for slot '%s', well '%s': %s", slot, well, pos)
393
434
  else:
394
435
  self._logger.debug("Absolute Z position for slot '%s': %s", slot, pos)
@@ -404,15 +445,22 @@ class First:
404
445
 
405
446
  Returns:
406
447
  Position with absolute coordinates
448
+
449
+ Raises:
450
+ ValueError: If well is specified but no labware is loaded in the slot
407
451
  """
408
452
  pos = self.get_slot_origin(slot)
409
453
 
410
454
  if well:
411
- well_pos = self.deck[slot].get_well_position(well).get_xy()
455
+ labware = self.deck[slot]
456
+ if labware is None:
457
+ self._logger.error("Cannot get well position: no labware loaded in slot '%s'", slot)
458
+ raise ValueError(f"No labware loaded in slot '{slot}'. Load labware before accessing wells.")
459
+ well_pos = labware.get_well_position(well).get_xy()
412
460
  pos += well_pos.swap_xy()
413
461
 
414
462
  # get a
415
- a = Position(a=self.deck[slot].get_height() - self.CEILING_HEIGHT)
463
+ a = Position(a=labware.get_height() - self.CEILING_HEIGHT)
416
464
  pos += a
417
465
  self._logger.debug("Absolute A position for slot '%s', well '%s': %s", slot, well, pos)
418
466
  else:
puda_drivers/move/deck.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # src/puda_drivers/move/deck.py
2
2
 
3
+ import json
3
4
  from puda_drivers.labware import StandardLabware
4
5
 
5
6
 
@@ -29,6 +30,20 @@ class Deck:
29
30
  if slot.upper() not in self.slots:
30
31
  raise KeyError(f"Slot {slot} not found in deck")
31
32
  self.slots[slot.upper()] = StandardLabware(labware_name=labware_name)
33
+
34
+ def empty_slot(self, slot: str):
35
+ """
36
+ Empty a slot (remove labware from it).
37
+
38
+ Args:
39
+ slot: Slot name (e.g., 'A1', 'B2')
40
+
41
+ Raises:
42
+ KeyError: If slot is not found in deck
43
+ """
44
+ if slot.upper() not in self.slots:
45
+ raise KeyError(f"Slot {slot} not found in deck")
46
+ self.slots[slot.upper()] = None
32
47
 
33
48
  def __str__(self):
34
49
  """
@@ -44,4 +59,23 @@ class Deck:
44
59
 
45
60
  def __getitem__(self, key):
46
61
  """Allows syntax for: my_deck['B4']"""
47
- return self.slots[key.upper()]
62
+ return self.slots[key.upper()]
63
+
64
+ def to_dict(self) -> dict:
65
+ """
66
+ Return the deck layout as a dictionary.
67
+ """
68
+ deck_data = {}
69
+ for slot, labware in self.slots.items():
70
+ if labware is None:
71
+ deck_data[slot] = None
72
+ else:
73
+ deck_data[slot] = labware.name
74
+ return deck_data
75
+
76
+ def to_json(self) -> str:
77
+ """
78
+ Return the deck layout as a JSON string.
79
+ """
80
+ # Re-use the logic from to_dict() so you don't have to update it in two places
81
+ return json.dumps(self.to_dict(), indent=2)
@@ -296,7 +296,7 @@ class GCodeController(SerialController):
296
296
  home_target = "All"
297
297
 
298
298
  self._logger.info("[%s] homing axis/axes: %s **", cmd, home_target)
299
- self.execute(cmd)
299
+ self.execute(command=cmd, timeout=90) # 90 seconds timeout for homing
300
300
  self._logger.info("Homing of %s completed.\n", home_target)
301
301
 
302
302
  # Update internal position (optimistic zeroing)
@@ -514,6 +514,9 @@ class GCodeController(SerialController):
514
514
 
515
515
  This method can be called even when the machine is moving from other commands,
516
516
  as it runs the blocking serial communication in a separate thread.
517
+
518
+ If the M114 command takes more than 1 second to respond, falls back to
519
+ returning the internally tracked position instead.
517
520
 
518
521
  Returns:
519
522
  Position containing X, Y, Z, and A positions
@@ -523,28 +526,38 @@ class GCodeController(SerialController):
523
526
  """
524
527
  self._logger.info("Querying current machine position (M114).")
525
528
  # Run the blocking execute call in a thread pool to allow concurrent operations
526
- res: str = await asyncio.to_thread(self.execute, "M114")
527
-
528
- # Extract position values using regex
529
- pattern = re.compile(r"([XYZA]):(-?\d+\.\d+)")
530
- matches = pattern.findall(res)
529
+ # with a 1 second timeout - if it takes longer, fall back to internal position
530
+ try:
531
+ res: str = await asyncio.wait_for(
532
+ asyncio.to_thread(self.execute, "M114"),
533
+ timeout=1.0
534
+ )
535
+
536
+ # Extract position values using regex
537
+ pattern = re.compile(r"([XYZA]):(-?\d+\.\d+)")
538
+ matches = pattern.findall(res)
531
539
 
532
- position_data: Dict[str, float] = {}
540
+ position_data: Dict[str, float] = {}
533
541
 
534
- for axis, value_str in matches:
535
- try:
536
- position_data[axis.lower()] = float(value_str)
537
- except ValueError:
538
- self._logger.error(
539
- "Failed to convert position value '%s' for axis %s to float.",
540
- value_str,
541
- axis,
542
- )
543
- continue
544
-
545
- position = Position.from_dict(position_data)
546
- self._logger.info("Query position complete. Retrieved positions: %s", position)
547
- return position
542
+ for axis, value_str in matches:
543
+ try:
544
+ position_data[axis.lower()] = float(value_str)
545
+ except ValueError:
546
+ self._logger.error(
547
+ "Failed to convert position value '%s' for axis %s to float.",
548
+ value_str,
549
+ axis,
550
+ )
551
+ continue
552
+
553
+ position = Position.from_dict(position_data)
554
+ self._logger.info("Query position complete. Retrieved positions: %s", position)
555
+ return position
556
+ except asyncio.TimeoutError:
557
+ self._logger.info(
558
+ "M114 command timed out after 1 second. Falling back to internal position."
559
+ )
560
+ return self.get_internal_position()
548
561
 
549
562
  def _sync_position(self) -> Tuple[bool, Position]:
550
563
  """
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: puda-drivers
3
- Version: 0.0.15
3
+ Version: 0.0.17
4
4
  Summary: Hardware drivers for the PUDA platform.
5
- Project-URL: Homepage, https://github.com/zhao-bears/puda-drivers
6
- Project-URL: Issues, https://github.com/zhao-bears/puda-drivers/issues
5
+ Project-URL: Homepage, https://github.com/PUDAP/puda-drivers
6
+ Project-URL: Issues, https://github.com/PUDAP/puda-drivers/issues
7
7
  Author-email: zhao <20024592+agentzhao@users.noreply.github.com>
8
8
  License-Expression: MIT
9
9
  License-File: LICENSE
@@ -18,6 +18,7 @@ Requires-Python: >=3.10
18
18
  Requires-Dist: nats-py>=2.12.0
19
19
  Requires-Dist: opencv-python>=4.12.0.88
20
20
  Requires-Dist: pyserial~=3.5
21
+ Requires-Dist: pytest>=9.0.2
21
22
  Description-Content-Type: text/markdown
22
23
 
23
24
  # puda-drivers
@@ -40,14 +41,6 @@ Hardware drivers for the PUDA (Physical Unified Device Architecture) platform. T
40
41
  pip install puda-drivers
41
42
  ```
42
43
 
43
- ### From Source
44
-
45
- ```bash
46
- git clone https://github.com/zhao-bears/puda-drivers.git
47
- cd puda-drivers
48
- pip install -e .
49
- ```
50
-
51
44
  ## Quick Start
52
45
 
53
46
  ### Logging Configuration
@@ -197,22 +190,70 @@ sartorius_ports = list_serial_ports(filter_desc="Sartorius")
197
190
 
198
191
  ### Setup Development Environment
199
192
 
200
- First, install `uv` if you haven't already. See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/) for platform-specific instructions.
193
+ This package is part of a UV workspace monorepo. First, install `uv` if you haven't already. See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/) for platform-specific instructions.
194
+
195
+ **From the repository root:**
201
196
 
202
197
  ```bash
203
- # Create virtual environment
204
- uv venv
198
+ # Or install dependencies for all workspace packages
199
+ uv sync --all-packages
200
+ ```
201
+
202
+ This will:
203
+ - Create a virtual environment at the repository root (`.venv/`)
204
+ - Install all dependencies for all workspace packages
205
+ - Install `puda-drivers` and other workspace packages in editable mode automatically
205
206
 
206
- # Activate virtual environment
207
+ **Using the package:**
208
+
209
+ ```bash
210
+ # Run Python scripts with workspace context (recommended, works from anywhere in the workspace)
211
+ uv run python your_script.py
212
+
213
+ # Or activate the virtual environment (from repository root where .venv is located)
207
214
  source .venv/bin/activate # On Windows: .venv\Scripts\activate
215
+ python your_script.py
216
+ ```
217
+
218
+ **Adding dependencies:**
208
219
 
209
- # Install dependencies
210
- uv sync
220
+ ```bash
221
+ # From the package directory
222
+ cd libs/drivers
223
+ uv add some-package
211
224
 
212
- # Install package in editable mode
213
- pip install -e .
225
+ # Or from repository root
226
+ uv add --package puda-drivers some-package
214
227
  ```
215
228
 
229
+ **Note:** Workspace packages are automatically installed in editable mode, so code changes are immediately available without reinstalling.
230
+
231
+ ### Testing
232
+
233
+ Run tests using pytest with `uv run`:
234
+
235
+ ```bash
236
+ # Run all tests
237
+ uv run pytest tests/
238
+
239
+ # Run a specific test file
240
+ uv run pytest tests/test_deck.py
241
+
242
+ # Run a specific test class
243
+ uv run pytest tests/test_deck.py::TestDeckToDict
244
+
245
+ # Run a specific test function
246
+ uv run pytest tests/test_deck.py::TestDeckToDict::test_to_dict_empty_deck
247
+
248
+ # Run with verbose output
249
+ uv run pytest tests/ -v
250
+
251
+ # Run with coverage report
252
+ uv run pytest tests/ --cov=puda_drivers --cov-report=html
253
+ ```
254
+
255
+ **Note:** Make sure you're in the `libs/drivers` directory or use the full path to the tests directory when running pytest commands.
256
+
216
257
  ### Building and Publishing
217
258
 
218
259
  ```bash
@@ -3,19 +3,19 @@ puda_drivers/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  puda_drivers/core/__init__.py,sha256=XbCdXsU6NMDsmEAtavAGiSZZPla5d7zc2L7Qx9qKHdY,214
4
4
  puda_drivers/core/logging.py,sha256=prOeJ3CGEbm37TtMRyAOTQQiMU5_ImZTRXmcUJxkenc,2892
5
5
  puda_drivers/core/position.py,sha256=f4efmDSrKKCtqrR-GUJxVitPG20MiuGSDOWt-9TVISk,12628
6
- puda_drivers/core/serialcontroller.py,sha256=38mKas1iJaOkAE0_V4tmqgZz7RxMMEWfGqA0Ma_Dt2A,8604
6
+ puda_drivers/core/serialcontroller.py,sha256=8Yf9DYIhtQTIVGB1Pm6COa6qgJcM0ZxdGwOGW8Om9ss,8654
7
7
  puda_drivers/cv/__init__.py,sha256=DYiPwYOLSUsZ9ivWiHoMGGD3MZ3Ygu9qLYja_OiUodU,100
8
8
  puda_drivers/cv/camera.py,sha256=Tzxt1h3W5Z8XFanud_z-PhhJ2UYsC4OEhcak9TVu8jM,16490
9
9
  puda_drivers/labware/__init__.py,sha256=RlRxrJn2zyzyxv4c1KGt8Gmxv2cRO8V4zZVnnyL-I00,288
10
10
  puda_drivers/labware/labware.py,sha256=hZhOzSyb1GP_bm1LvQsREx4YSqLCWBTKNWDgDqfFavI,5317
11
11
  puda_drivers/labware/opentrons_96_tiprack_300ul.json,sha256=jmNaworu688GEgFdxMxNRSaEp4ZOg9IumFk8bVHSzYY,19796
12
12
  puda_drivers/labware/polyelectric_8_wellplate_30000ul.json,sha256=esu2tej0ORs7Pfd4HwoQVUpU5mPvp2AYzE3zsCC2FDk,3104
13
- puda_drivers/labware/trash_bin.json,sha256=Hk4MXO48P28jG7F87DUd9Ja4c_P7kAy3karPQ965i9Y,580
13
+ puda_drivers/labware/trash_bin.json,sha256=6dH0prXKJVXMRm096rPSiJgMr0nPRtHUKo7Z6xIxdnA,596
14
14
  puda_drivers/machines/__init__.py,sha256=zmIk_r2T8nbPA68h3Cko8N6oL7ncoBpmvhNcAqzHmc4,45
15
- puda_drivers/machines/first.py,sha256=cqK8Gwk89nTweVTImhHU1RimtvDn4En36qV_ZanMGIU,19701
15
+ puda_drivers/machines/first.py,sha256=GYYEUGi6xx6ev-ByX-_y9_GnMaBdhPCeiu-bCnKDZw0,21417
16
16
  puda_drivers/move/__init__.py,sha256=NKIKckcqgyviPM0EGFcmIoaqkJM4qekR4babfdddRzM,96
17
- puda_drivers/move/deck.py,sha256=yq2B4WMqj0hQvHt8HoJskP10u1DUyKwUnjP2c9gJ174,1397
18
- puda_drivers/move/gcode.py,sha256=GNbFkGgXcr0vBXXWbM96aRkkdQsTiqLCA56V-z3ebYs,23102
17
+ puda_drivers/move/deck.py,sha256=CprlB8i9jvNImrc5IpHUZSSvvFe8uei76fNQ4iN6n34,2394
18
+ puda_drivers/move/gcode.py,sha256=ioqRS-Kom5S7Pyp9-wg4MRXGIbIDEz7SsihBBOM2m30,23784
19
19
  puda_drivers/move/grbl/__init__.py,sha256=vBeeti8DVN2dACi1rLmHN_UGIOdo0s-HZX6mIepLV5I,98
20
20
  puda_drivers/move/grbl/api.py,sha256=loj8_Vap7S9qaD0ReHhgxr9Vkl6Wp7DGzyLkZyZ6v_k,16995
21
21
  puda_drivers/move/grbl/constants.py,sha256=4736CRDzLGWVqGscLajMlrIQMyubsHfthXi4RF1CHNg,9585
@@ -23,7 +23,7 @@ puda_drivers/transfer/liquid/sartorius/__init__.py,sha256=7fljIbu0KkumsI3NI3O64d
23
23
  puda_drivers/transfer/liquid/sartorius/api.py,sha256=jxwIJmY2k1K2ts6NC2ZgFTe4MOiH8TGnJeqYOqNa3rE,28250
24
24
  puda_drivers/transfer/liquid/sartorius/constants.py,sha256=mcsjLrVBH-RSodH-pszstwcEL9wwbV0vOgHbGNxZz9w,2770
25
25
  puda_drivers/transfer/liquid/sartorius/rLine.py,sha256=FWEFO9tZN3cbneiozXRs77O-47Jg_8vYzWJvUrWpYxA,14531
26
- puda_drivers-0.0.15.dist-info/METADATA,sha256=CBB25oLMTKaHkRqnPsrr80dDLenQxKa_OQoZo0GTSno,7102
27
- puda_drivers-0.0.15.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
28
- puda_drivers-0.0.15.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
29
- puda_drivers-0.0.15.dist-info/RECORD,,
26
+ puda_drivers-0.0.17.dist-info/METADATA,sha256=YFBVlnKBI0al6Z6F9w0urGImcN133jmzsJVTcojNfEc,8420
27
+ puda_drivers-0.0.17.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
28
+ puda_drivers-0.0.17.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
29
+ puda_drivers-0.0.17.dist-info/RECORD,,