puda-drivers 0.0.16__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.
@@ -48,7 +48,7 @@ class SerialController(ABC):
48
48
  self._serial = None
49
49
  self.port_name = port_name
50
50
  self.baudrate = baudrate
51
- self.timeout = timeout
51
+ self._timeout = timeout
52
52
  self._logger = logger
53
53
 
54
54
  # lock to prevent concurrent access to the serial port
@@ -75,7 +75,7 @@ class SerialController(ABC):
75
75
  self._serial = serial.Serial(
76
76
  port=self.port_name,
77
77
  baudrate=self.baudrate,
78
- timeout=self.timeout,
78
+ timeout=self._timeout,
79
79
  )
80
80
  self._serial.flush()
81
81
  self._logger.info("Successfully connected to %s.", self.port_name)
@@ -107,19 +107,19 @@ class SerialController(ABC):
107
107
 
108
108
  def _send_command(self, command: str) -> None:
109
109
  """
110
- Sends a custom protocol command.
110
+ Sends a command to the device.
111
111
  Note: This method should be called while holding self._lock to ensure
112
112
  atomic command/response pairing.
113
113
  """
114
+ self._logger.info("-> Sending: %r", command)
115
+
114
116
  if not self.is_connected or not self._serial:
115
117
  self._logger.error(
116
118
  "Attempt to send command '%s' failed: Device not connected.",
117
119
  command,
118
120
  )
119
121
  # Retain raising an error for being disconnected, as that's a connection state issue
120
- raise serial.SerialException("Device not connected. Call connect() first.")
121
-
122
- self._logger.info("-> Sending: %r", command)
122
+ raise serial.SerialException("Device disconnected. Call connect() first.")
123
123
 
124
124
  try:
125
125
  self._serial.reset_input_buffer() # clear input buffer
@@ -140,7 +140,7 @@ class SerialController(ABC):
140
140
  )
141
141
  return None
142
142
 
143
- def _read_response(self) -> str:
143
+ def _read_response(self, timeout: int = None) -> str:
144
144
  """
145
145
  Generic, blocking read that respects timeout and returns
146
146
  all data that arrived within the timeout period.
@@ -148,10 +148,13 @@ class SerialController(ABC):
148
148
  if not self.is_connected or not self._serial:
149
149
  raise serial.SerialException("Device not connected.")
150
150
 
151
+ if timeout is None:
152
+ timeout = self._timeout
153
+
151
154
  start_time = time.time()
152
155
  response = b""
153
156
 
154
- while time.time() - start_time < self.timeout:
157
+ while time.time() - start_time < timeout:
155
158
  if self._serial.in_waiting > 0:
156
159
  # Read all available bytes
157
160
  response += self._serial.read(self._serial.in_waiting)
@@ -168,14 +171,12 @@ class SerialController(ABC):
168
171
 
169
172
  # Timeout reached - check what we got
170
173
  if not response:
171
- self._logger.error("No response within %s seconds.", self.timeout)
174
+ self._logger.error("No response within %s seconds.", timeout)
172
175
  raise serial.SerialTimeoutException(
173
- f"No response received within {self.timeout} seconds."
176
+ f"No response received within {timeout} seconds."
174
177
  )
175
178
 
176
- # Decode once and check the decoded string
177
179
  decoded_response = response.decode("utf-8", errors="ignore").strip()
178
-
179
180
  if "ok" in decoded_response.lower():
180
181
  self._logger.debug("<- Received response: %r", decoded_response)
181
182
  elif "err" in decoded_response.lower():
@@ -193,10 +194,12 @@ class SerialController(ABC):
193
194
  def _build_command(self, command: str, value: Optional[str] = None) -> str:
194
195
  """
195
196
  Build a command string according to the device protocol.
197
+
198
+ There might be special starting and ending characters for devices
196
199
  """
197
- pass
200
+ raise NotImplementedError
198
201
 
199
- def execute(self, command: str, value: Optional[str] = None) -> str:
202
+ def execute(self, command: str, value: Optional[str] = None, timeout: int = None) -> str:
200
203
  """
201
204
  Send a command and read the response atomically.
202
205
 
@@ -218,22 +221,10 @@ class SerialController(ABC):
218
221
  serial.SerialException: If device is not connected or communication fails
219
222
  serial.SerialTimeoutException: If no response is received within timeout
220
223
  """
224
+ if timeout is None:
225
+ timeout = self._timeout
221
226
  # Hold the lock for the entire send+read operation to ensure atomicity
222
227
  # This prevents concurrent commands from mixing up responses
223
228
  with self._lock:
224
- # Increase timeout by 60 seconds for G28 (homing) command
225
- original_timeout = self.timeout
226
- if "G28" in command.upper():
227
- self.timeout = original_timeout + 60
228
- # Also update the serial connection's timeout if connected
229
- if self.is_connected and self._serial:
230
- self._serial.timeout = self.timeout
231
-
232
- try:
233
- self._send_command(self._build_command(command, value))
234
- return self._read_response()
235
- finally:
236
- # Restore original timeout
237
- self.timeout = original_timeout
238
- if self.is_connected and self._serial:
239
- self._serial.timeout = original_timeout
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": [
@@ -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.
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)
@@ -554,7 +554,7 @@ class GCodeController(SerialController):
554
554
  self._logger.info("Query position complete. Retrieved positions: %s", position)
555
555
  return position
556
556
  except asyncio.TimeoutError:
557
- self._logger.warning(
557
+ self._logger.info(
558
558
  "M114 command timed out after 1 second. Falling back to internal position."
559
559
  )
560
560
  return self.get_internal_position()
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: puda-drivers
3
- Version: 0.0.16
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=aIyEpA8J4R7yBfIq0roMqbD-nxEwuwaB8c_7zaOXg_A,9100
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=5xFjMwCHM4wZz2_VjEMXPMyXndtXjcnt1WL_GWSSRbs,20725
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=nS1dmt90_89BUjlHh0VNqGLeRhyIdJ4-8Ps_7YlW-B0,23735
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.16.dist-info/METADATA,sha256=kD7z3BQmYQAihFsf7T_f_iL3ttiLyY4nDegdEfmuElk,7102
27
- puda_drivers-0.0.16.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
28
- puda_drivers-0.0.16.dist-info/licenses/LICENSE,sha256=7EI8xVBu6h_7_JlVw-yPhhOZlpY9hP8wal7kHtqKT_E,1074
29
- puda_drivers-0.0.16.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,,