puda-drivers 0.0.16__tar.gz → 0.0.17__tar.gz

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.
Files changed (40) hide show
  1. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/PKG-INFO +60 -19
  2. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/README.md +56 -16
  3. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/pyproject.toml +4 -3
  4. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/core/serialcontroller.py +21 -30
  5. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/labware/trash_bin.json +2 -1
  6. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/machines/first.py +30 -0
  7. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/move/deck.py +35 -1
  8. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/move/gcode.py +2 -2
  9. puda_drivers-0.0.17/tests/poll_position.py +126 -0
  10. puda_drivers-0.0.17/tests/test_deck.py +172 -0
  11. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/tests/test_position_polling.py +3 -3
  12. puda_drivers-0.0.17/tests/webcam.py +59 -0
  13. puda_drivers-0.0.16/tests/webcam.py +0 -28
  14. puda_drivers-0.0.16/uv.lock +0 -114
  15. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/.gitignore +0 -0
  16. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/LICENSE +0 -0
  17. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/__init__.py +0 -0
  18. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/core/__init__.py +0 -0
  19. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/core/logging.py +0 -0
  20. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/core/position.py +0 -0
  21. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/cv/__init__.py +0 -0
  22. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/cv/camera.py +0 -0
  23. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/labware/__init__.py +0 -0
  24. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/labware/labware.py +0 -0
  25. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/labware/opentrons_96_tiprack_300ul.json +0 -0
  26. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/labware/polyelectric_8_wellplate_30000ul.json +0 -0
  27. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/machines/__init__.py +0 -0
  28. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/move/__init__.py +0 -0
  29. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/move/grbl/__init__.py +0 -0
  30. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/move/grbl/api.py +0 -0
  31. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/move/grbl/constants.py +0 -0
  32. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/py.typed +0 -0
  33. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/transfer/liquid/sartorius/__init__.py +0 -0
  34. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/transfer/liquid/sartorius/api.py +0 -0
  35. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/transfer/liquid/sartorius/constants.py +0 -0
  36. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/src/puda_drivers/transfer/liquid/sartorius/rLine.py +0 -0
  37. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/tests/example.py +0 -0
  38. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/tests/first.py +0 -0
  39. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/tests/pipette.py +0 -0
  40. {puda_drivers-0.0.16 → puda_drivers-0.0.17}/tests/qubot.py +0 -0
@@ -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
@@ -18,14 +18,6 @@ Hardware drivers for the PUDA (Physical Unified Device Architecture) platform. T
18
18
  pip install puda-drivers
19
19
  ```
20
20
 
21
- ### From Source
22
-
23
- ```bash
24
- git clone https://github.com/zhao-bears/puda-drivers.git
25
- cd puda-drivers
26
- pip install -e .
27
- ```
28
-
29
21
  ## Quick Start
30
22
 
31
23
  ### Logging Configuration
@@ -175,22 +167,70 @@ sartorius_ports = list_serial_ports(filter_desc="Sartorius")
175
167
 
176
168
  ### Setup Development Environment
177
169
 
178
- 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.
170
+ 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.
171
+
172
+ **From the repository root:**
179
173
 
180
174
  ```bash
181
- # Create virtual environment
182
- uv venv
175
+ # Or install dependencies for all workspace packages
176
+ uv sync --all-packages
177
+ ```
178
+
179
+ This will:
180
+ - Create a virtual environment at the repository root (`.venv/`)
181
+ - Install all dependencies for all workspace packages
182
+ - Install `puda-drivers` and other workspace packages in editable mode automatically
183
183
 
184
- # Activate virtual environment
184
+ **Using the package:**
185
+
186
+ ```bash
187
+ # Run Python scripts with workspace context (recommended, works from anywhere in the workspace)
188
+ uv run python your_script.py
189
+
190
+ # Or activate the virtual environment (from repository root where .venv is located)
185
191
  source .venv/bin/activate # On Windows: .venv\Scripts\activate
192
+ python your_script.py
193
+ ```
194
+
195
+ **Adding dependencies:**
186
196
 
187
- # Install dependencies
188
- uv sync
197
+ ```bash
198
+ # From the package directory
199
+ cd libs/drivers
200
+ uv add some-package
189
201
 
190
- # Install package in editable mode
191
- pip install -e .
202
+ # Or from repository root
203
+ uv add --package puda-drivers some-package
192
204
  ```
193
205
 
206
+ **Note:** Workspace packages are automatically installed in editable mode, so code changes are immediately available without reinstalling.
207
+
208
+ ### Testing
209
+
210
+ Run tests using pytest with `uv run`:
211
+
212
+ ```bash
213
+ # Run all tests
214
+ uv run pytest tests/
215
+
216
+ # Run a specific test file
217
+ uv run pytest tests/test_deck.py
218
+
219
+ # Run a specific test class
220
+ uv run pytest tests/test_deck.py::TestDeckToDict
221
+
222
+ # Run a specific test function
223
+ uv run pytest tests/test_deck.py::TestDeckToDict::test_to_dict_empty_deck
224
+
225
+ # Run with verbose output
226
+ uv run pytest tests/ -v
227
+
228
+ # Run with coverage report
229
+ uv run pytest tests/ --cov=puda_drivers --cov-report=html
230
+ ```
231
+
232
+ **Note:** Make sure you're in the `libs/drivers` directory or use the full path to the tests directory when running pytest commands.
233
+
194
234
  ### Building and Publishing
195
235
 
196
236
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "puda-drivers"
3
- version = "0.0.16"
3
+ version = "0.0.17"
4
4
  description = "Hardware drivers for the PUDA platform."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -22,6 +22,7 @@ dependencies = [
22
22
  "nats-py>=2.12.0",
23
23
  "opencv-python>=4.12.0.88",
24
24
  "pyserial~=3.5",
25
+ "pytest>=9.0.2",
25
26
  ]
26
27
 
27
28
  [build-system]
@@ -29,5 +30,5 @@ requires = ["hatchling >= 1.26"]
29
30
  build-backend = "hatchling.build"
30
31
 
31
32
  [project.urls]
32
- Homepage = "https://github.com/zhao-bears/puda-drivers"
33
- Issues = "https://github.com/zhao-bears/puda-drivers/issues"
33
+ Homepage = "https://github.com/PUDAP/puda-drivers"
34
+ Issues = "https://github.com/PUDAP/puda-drivers/issues"
@@ -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.
@@ -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()
@@ -0,0 +1,126 @@
1
+ """Test script to poll get_position every second while movement commands are running."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import time
6
+ from datetime import datetime
7
+ from puda_drivers.machines import First
8
+ from puda_drivers.core import Position, setup_logging
9
+
10
+ setup_logging(
11
+ enable_file_logging=False,
12
+ log_level=logging.INFO,
13
+ )
14
+
15
+
16
+ async def poll_position(machine: First, stop_event: asyncio.Event):
17
+ """
18
+ Poll get_position every second and print the results.
19
+
20
+ Args:
21
+ machine: First machine instance
22
+ stop_event: Event to signal when to stop polling
23
+ """
24
+ poll_count = 0
25
+ while not stop_event.is_set():
26
+ try:
27
+ position = await machine.get_position()
28
+ poll_count += 1
29
+ timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
30
+
31
+ qubot_pos = position["qubot"]
32
+ pipette_pos = position["pipette"]
33
+
34
+ print(f"[{timestamp}] Poll #{poll_count}")
35
+ print(f" QuBot: X={qubot_pos.get('x', 0):.2f}, "
36
+ f"Y={qubot_pos.get('y', 0):.2f}, "
37
+ f"Z={qubot_pos.get('z', 0):.2f}, "
38
+ f"A={qubot_pos.get('a', 0):.2f}")
39
+ print(f" Pipette: {pipette_pos} steps")
40
+ print()
41
+
42
+ except Exception as e:
43
+ print(f"Error polling position: {e}")
44
+
45
+ # Wait 1 second before next poll
46
+ await asyncio.sleep(1.0)
47
+
48
+
49
+ def run_movements(machine: First):
50
+ """
51
+ Run a sequence of movement commands.
52
+
53
+ Args:
54
+ machine: First machine instance
55
+ """
56
+ print("Starting movement sequence...")
57
+
58
+ # Example movement sequence
59
+ positions = [
60
+ Position(x=50, y=-100, z=-50),
61
+ Position(x=150, y=-200, z=-50),
62
+ Position(x=250, y=-300, z=-50),
63
+ Position(x=150, y=-200, z=-50),
64
+ Position(x=50, y=-100, z=-50),
65
+ ]
66
+
67
+ for i, pos in enumerate(positions, 1):
68
+ print(f"Moving to position {i}/{len(positions)}: {pos}")
69
+ machine.qubot.move_absolute(position=pos)
70
+ time.sleep(0.5) # Small delay between movements
71
+
72
+ print("Movement sequence complete!")
73
+
74
+
75
+ async def main():
76
+ """Main function to run position polling and movements concurrently."""
77
+ # Initialize machine
78
+ machine = First(
79
+ qubot_port="/dev/ttyACM0",
80
+ sartorius_port="/dev/ttyUSB0",
81
+ camera_index=0,
82
+ )
83
+
84
+ try:
85
+ # Startup machine
86
+ print("Starting up machine...")
87
+ machine.startup()
88
+ print("Machine ready!\n")
89
+
90
+ # Create stop event for polling
91
+ stop_event = asyncio.Event()
92
+
93
+ # Start position polling task
94
+ polling_task = asyncio.create_task(poll_position(machine, stop_event))
95
+
96
+ # Run movements in a thread pool (since they're blocking)
97
+ print("Running movements in background...\n")
98
+ await asyncio.to_thread(run_movements, machine)
99
+
100
+ # Wait a bit more to see final position
101
+ await asyncio.sleep(2)
102
+
103
+ # Stop polling
104
+ stop_event.set()
105
+ await polling_task
106
+
107
+ print("\nTest complete!")
108
+
109
+ except KeyboardInterrupt:
110
+ print("\nInterrupted by user")
111
+ stop_event.set()
112
+ await polling_task
113
+ except Exception as e:
114
+ print(f"\nError: {e}")
115
+ stop_event.set()
116
+ if not polling_task.done():
117
+ await polling_task
118
+ finally:
119
+ # Shutdown machine
120
+ print("\nShutting down machine...")
121
+ machine.shutdown()
122
+
123
+
124
+ if __name__ == "__main__":
125
+ asyncio.run(main())
126
+
@@ -0,0 +1,172 @@
1
+ """Tests for Deck class to_dict() and to_json() methods."""
2
+
3
+ import json
4
+ from puda_drivers.move.deck import Deck
5
+
6
+
7
+ class TestDeckToDict:
8
+ """Test cases for Deck.to_dict() method."""
9
+
10
+ def test_to_dict_empty_deck(self):
11
+ """Test to_dict() on an empty deck (all slots are None)."""
12
+ deck = Deck(rows=2, cols=2)
13
+ result = deck.to_dict()
14
+
15
+ # Should have all slots
16
+ assert len(result) == 4
17
+ assert result["A1"] is None
18
+ assert result["A2"] is None
19
+ assert result["B1"] is None
20
+ assert result["B2"] is None
21
+
22
+ def test_to_dict_with_labware(self):
23
+ """Test to_dict() on a deck with labware loaded."""
24
+ deck = Deck(rows=2, cols=2)
25
+ deck.load_labware("A1", "opentrons_96_tiprack_300ul")
26
+ deck.load_labware("B2", "trash_bin")
27
+
28
+ result = deck.to_dict()
29
+
30
+ # Check that loaded labware have their names
31
+ assert result["A1"] == "Opentrons OT-2 96 Tip Rack 300 µL"
32
+ assert result["B2"] == "Trash Bin"
33
+
34
+ # Check that unloaded slots are None
35
+ assert result["A2"] is None
36
+ assert result["B1"] is None
37
+
38
+ def test_to_dict_mixed_slots(self):
39
+ """Test to_dict() with a mix of loaded and empty slots."""
40
+ deck = Deck(rows=3, cols=3)
41
+ deck.load_labware("A1", "opentrons_96_tiprack_300ul")
42
+ deck.load_labware("C3", "polyelectric_8_wellplate_30000ul")
43
+
44
+ result = deck.to_dict()
45
+
46
+ # Should have all 9 slots
47
+ assert len(result) == 9
48
+
49
+ # Check loaded slots
50
+ assert result["A1"] == "Opentrons OT-2 96 Tip Rack 300 µL"
51
+ assert result["C3"] == "Polyelectric 8 Well Plate 30000 µL"
52
+
53
+ # Check some empty slots
54
+ assert result["A2"] is None
55
+ assert result["B1"] is None
56
+ assert result["B2"] is None
57
+ assert result["C1"] is None
58
+
59
+ def test_to_dict_all_slots_filled(self):
60
+ """Test to_dict() when all slots are filled."""
61
+ deck = Deck(rows=2, cols=2)
62
+ deck.load_labware("A1", "opentrons_96_tiprack_300ul")
63
+ deck.load_labware("A2", "trash_bin")
64
+ deck.load_labware("B1", "polyelectric_8_wellplate_30000ul")
65
+ deck.load_labware("B2", "opentrons_96_tiprack_300ul")
66
+
67
+ result = deck.to_dict()
68
+
69
+ # All slots should have labware names
70
+ assert result["A1"] == "Opentrons OT-2 96 Tip Rack 300 µL"
71
+ assert result["A2"] == "Trash Bin"
72
+ assert result["B1"] == "Polyelectric 8 Well Plate 30000 µL"
73
+ assert result["B2"] == "Opentrons OT-2 96 Tip Rack 300 µL"
74
+
75
+ # No None values
76
+ assert None not in result.values()
77
+
78
+
79
+ class TestDeckToJson:
80
+ """Test cases for Deck.to_json() method."""
81
+
82
+ def test_to_json_empty_deck(self):
83
+ """Test to_json() on an empty deck."""
84
+ deck = Deck(rows=2, cols=2)
85
+ json_str = deck.to_json()
86
+
87
+ # Should be valid JSON
88
+ parsed = json.loads(json_str)
89
+ assert isinstance(parsed, dict)
90
+
91
+ # Should match to_dict() output
92
+ assert parsed == deck.to_dict()
93
+
94
+ # Should have proper indentation (check for newlines)
95
+ assert "\n" in json_str
96
+
97
+ def test_to_json_with_labware(self):
98
+ """Test to_json() on a deck with labware loaded."""
99
+ deck = Deck(rows=2, cols=2)
100
+ deck.load_labware("A1", "opentrons_96_tiprack_300ul")
101
+ deck.load_labware("B2", "trash_bin")
102
+
103
+ json_str = deck.to_json()
104
+
105
+ # Should be valid JSON
106
+ parsed = json.loads(json_str)
107
+ assert isinstance(parsed, dict)
108
+
109
+ # Should match to_dict() output exactly
110
+ assert parsed == deck.to_dict()
111
+
112
+ # Should contain labware names in the parsed JSON (checking parsed values
113
+ # instead of raw string since JSON serialization may escape Unicode characters)
114
+ assert parsed["A1"] == "Opentrons OT-2 96 Tip Rack 300 µL"
115
+ assert parsed["B2"] == "Trash Bin"
116
+
117
+ def test_to_json_matches_to_dict(self):
118
+ """Test that to_json() when parsed matches to_dict() output."""
119
+ deck = Deck(rows=3, cols=3)
120
+ deck.load_labware("A1", "opentrons_96_tiprack_300ul")
121
+ deck.load_labware("B2", "trash_bin")
122
+ deck.load_labware("C3", "polyelectric_8_wellplate_30000ul")
123
+
124
+ dict_result = deck.to_dict()
125
+ json_str = deck.to_json()
126
+ json_result = json.loads(json_str)
127
+
128
+ # Should be identical
129
+ assert dict_result == json_result
130
+
131
+ # Verify structure
132
+ assert json_result["A1"] == "Opentrons OT-2 96 Tip Rack 300 µL"
133
+ assert json_result["B2"] == "Trash Bin"
134
+ assert json_result["C3"] == "Polyelectric 8 Well Plate 30000 µL"
135
+ assert json_result["A2"] is None
136
+
137
+ def test_to_json_indentation(self):
138
+ """Test that to_json() has proper indentation (indent=2)."""
139
+ deck = Deck(rows=2, cols=2)
140
+ deck.load_labware("A1", "opentrons_96_tiprack_300ul")
141
+
142
+ json_str = deck.to_json()
143
+
144
+ # Check that it has indentation (2 spaces per level)
145
+ lines = json_str.split("\n")
146
+ # First line should be "{"
147
+ assert lines[0].strip() == "{"
148
+ # Second line should have 2 spaces of indentation
149
+ assert lines[1].startswith(" ")
150
+
151
+ # Verify it's still valid JSON
152
+ parsed = json.loads(json_str)
153
+ assert isinstance(parsed, dict)
154
+
155
+ def test_to_json_round_trip(self):
156
+ """Test that to_json() output can be loaded and matches original."""
157
+ deck = Deck(rows=2, cols=2)
158
+ deck.load_labware("A1", "opentrons_96_tiprack_300ul")
159
+ deck.load_labware("B2", "trash_bin")
160
+
161
+ original_dict = deck.to_dict()
162
+ json_str = deck.to_json()
163
+ loaded_dict = json.loads(json_str)
164
+
165
+ # Round trip should preserve all data
166
+ assert loaded_dict == original_dict
167
+
168
+ # Verify all keys and values match
169
+ for key in original_dict:
170
+ assert key in loaded_dict
171
+ assert loaded_dict[key] == original_dict[key]
172
+
@@ -3,7 +3,7 @@
3
3
  import asyncio
4
4
  import json
5
5
  import logging
6
- from datetime import datetime
6
+ from datetime import datetime, timezone
7
7
  from puda_drivers.machines import First
8
8
  from puda_drivers.labware import get_available_labware
9
9
  from puda_drivers.core import setup_logging
@@ -25,7 +25,7 @@ async def poll_position(machine: First, stop_event: asyncio.Event):
25
25
  while not stop_event.is_set():
26
26
  try:
27
27
  position = await machine.get_position()
28
- timestamp = datetime.utcnow().isoformat() + "Z"
28
+ timestamp = datetime.now(timezone.utc).isoformat()
29
29
  result = {
30
30
  "timestamp": timestamp,
31
31
  "qubot": position.get("qubot", {}),
@@ -33,7 +33,7 @@ async def poll_position(machine: First, stop_event: asyncio.Event):
33
33
  }
34
34
  print(json.dumps(result))
35
35
  except (ValueError, IOError, RuntimeError) as e:
36
- timestamp = datetime.utcnow().isoformat() + "Z"
36
+ timestamp = datetime.now(timezone.utc).isoformat()
37
37
  result = {
38
38
  "timestamp": timestamp,
39
39
  "qubot": {},
@@ -0,0 +1,59 @@
1
+ """Tests for camera controller functionality."""
2
+
3
+ import time
4
+ import pytest
5
+ from puda_drivers.cv import list_cameras, CameraController
6
+
7
+
8
+ @pytest.fixture
9
+ def camera_controller():
10
+ """Fixture to create and connect a camera controller."""
11
+ cam = CameraController(camera_index=4)
12
+ cam.connect()
13
+ yield cam
14
+ if cam.is_connected:
15
+ cam.disconnect()
16
+
17
+
18
+ def test_list_cameras():
19
+ """Test listing available cameras."""
20
+ cameras = list_cameras()
21
+ assert isinstance(cameras, list)
22
+ print(f"Found {len(cameras)} cameras")
23
+
24
+
25
+ # pylint: disable=redefined-outer-name
26
+ def test_camera_connection(camera_controller):
27
+ """Test camera connection."""
28
+ assert camera_controller.is_connected, "Camera should be connected"
29
+ print("Camera is connected")
30
+
31
+
32
+ # pylint: disable=redefined-outer-name
33
+ def test_capture_image(camera_controller):
34
+ """Test image capture functionality."""
35
+ if camera_controller.is_connected:
36
+ image = camera_controller.capture_image()
37
+ assert image is not None
38
+ print("Image captured successfully")
39
+
40
+
41
+ # pylint: disable=redefined-outer-name
42
+ def test_record_video_duration(camera_controller):
43
+ """Test recording video for a specific duration."""
44
+ if camera_controller.is_connected:
45
+ video_path = camera_controller.record_video(duration_seconds=10)
46
+ assert video_path is not None
47
+ print(f"Video recorded: {video_path}")
48
+
49
+
50
+ # pylint: disable=redefined-outer-name
51
+ def test_start_stop_video_recording(camera_controller):
52
+ """Test starting and stopping video recording manually."""
53
+ if camera_controller.is_connected:
54
+ print("Camera is connected")
55
+ video_path = camera_controller.start_video_recording()
56
+ time.sleep(3)
57
+ stop_path = camera_controller.stop_video_recording()
58
+ assert stop_path is not None or video_path is not None
59
+ print("Video recording started and stopped successfully")
@@ -1,28 +0,0 @@
1
- import time
2
- from puda_drivers.cv import list_cameras, CameraController
3
-
4
-
5
- # print(list_cameras())
6
-
7
- camera = CameraController(camera_index=4)
8
- camera.connect()
9
-
10
- # Image capture
11
- # if camera.is_connected:
12
- # print("Camera is connected")
13
- # camera.capture_image()
14
- # camera.disconnect()
15
-
16
- # Video recording
17
- # if camera.is_connected:
18
- # print("Camera is connected")
19
- # camera.record_video(duration_seconds=10)
20
- # camera.disconnect()
21
-
22
- # video recording
23
- if camera.is_connected:
24
- print("Camera is connected")
25
- camera.start_video_recording()
26
- time.sleep(3)
27
- camera.stop_video_recording()
28
- camera.disconnect()
@@ -1,114 +0,0 @@
1
- version = 1
2
- revision = 3
3
- requires-python = ">=3.10"
4
-
5
- [[package]]
6
- name = "nats-py"
7
- version = "2.12.0"
8
- source = { registry = "https://pypi.org/simple" }
9
- sdist = { url = "https://files.pythonhosted.org/packages/71/c5/2564d917503fe8d68fe630c74bf6b678fbc15c01b58f2565894761010f57/nats_py-2.12.0.tar.gz", hash = "sha256:2981ca4b63b8266c855573fa7871b1be741f1889fd429ee657e5ffc0971a38a1", size = 119821, upload-time = "2025-10-31T05:27:31.247Z" }
10
-
11
- [[package]]
12
- name = "numpy"
13
- version = "2.2.6"
14
- source = { registry = "https://pypi.org/simple" }
15
- sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
16
- wheels = [
17
- { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
18
- { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
19
- { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
20
- { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
21
- { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
22
- { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
23
- { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
24
- { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
25
- { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
26
- { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
27
- { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
28
- { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
29
- { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
30
- { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
31
- { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
32
- { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
33
- { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
34
- { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
35
- { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
36
- { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
37
- { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
38
- { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
39
- { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
40
- { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
41
- { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
42
- { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
43
- { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
44
- { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
45
- { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
46
- { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
47
- { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
48
- { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
49
- { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
50
- { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
51
- { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
52
- { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
53
- { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
54
- { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
55
- { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
56
- { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
57
- { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
58
- { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
59
- { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
60
- { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
61
- { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
62
- { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
63
- { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
64
- { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
65
- { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
66
- { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
67
- { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
68
- { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
69
- { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
70
- { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
71
- ]
72
-
73
- [[package]]
74
- name = "opencv-python"
75
- version = "4.12.0.88"
76
- source = { registry = "https://pypi.org/simple" }
77
- dependencies = [
78
- { name = "numpy" },
79
- ]
80
- sdist = { url = "https://files.pythonhosted.org/packages/ac/71/25c98e634b6bdeca4727c7f6d6927b056080668c5008ad3c8fc9e7f8f6ec/opencv-python-4.12.0.88.tar.gz", hash = "sha256:8b738389cede219405f6f3880b851efa3415ccd674752219377353f017d2994d", size = 95373294, upload-time = "2025-07-07T09:20:52.389Z" }
81
- wheels = [
82
- { url = "https://files.pythonhosted.org/packages/85/68/3da40142e7c21e9b1d4e7ddd6c58738feb013203e6e4b803d62cdd9eb96b/opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:f9a1f08883257b95a5764bf517a32d75aec325319c8ed0f89739a57fae9e92a5", size = 37877727, upload-time = "2025-07-07T09:13:31.47Z" },
83
- { url = "https://files.pythonhosted.org/packages/33/7c/042abe49f58d6ee7e1028eefc3334d98ca69b030e3b567fe245a2b28ea6f/opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:812eb116ad2b4de43ee116fcd8991c3a687f099ada0b04e68f64899c09448e81", size = 57326471, upload-time = "2025-07-07T09:13:41.26Z" },
84
- { url = "https://files.pythonhosted.org/packages/62/3a/440bd64736cf8116f01f3b7f9f2e111afb2e02beb2ccc08a6458114a6b5d/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:51fd981c7df6af3e8f70b1556696b05224c4e6b6777bdd2a46b3d4fb09de1a92", size = 45887139, upload-time = "2025-07-07T09:13:50.761Z" },
85
- { url = "https://files.pythonhosted.org/packages/68/1f/795e7f4aa2eacc59afa4fb61a2e35e510d06414dd5a802b51a012d691b37/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:092c16da4c5a163a818f120c22c5e4a2f96e0db4f24e659c701f1fe629a690f9", size = 67041680, upload-time = "2025-07-07T09:14:01.995Z" },
86
- { url = "https://files.pythonhosted.org/packages/02/96/213fea371d3cb2f1d537612a105792aa0a6659fb2665b22cad709a75bd94/opencv_python-4.12.0.88-cp37-abi3-win32.whl", hash = "sha256:ff554d3f725b39878ac6a2e1fa232ec509c36130927afc18a1719ebf4fbf4357", size = 30284131, upload-time = "2025-07-07T09:14:08.819Z" },
87
- { url = "https://files.pythonhosted.org/packages/fa/80/eb88edc2e2b11cd2dd2e56f1c80b5784d11d6e6b7f04a1145df64df40065/opencv_python-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:d98edb20aa932fd8ebd276a72627dad9dc097695b3d435a4257557bbb49a79d2", size = 39000307, upload-time = "2025-07-07T09:14:16.641Z" },
88
- ]
89
-
90
- [[package]]
91
- name = "puda-drivers"
92
- version = "0.0.16"
93
- source = { editable = "." }
94
- dependencies = [
95
- { name = "nats-py" },
96
- { name = "opencv-python" },
97
- { name = "pyserial" },
98
- ]
99
-
100
- [package.metadata]
101
- requires-dist = [
102
- { name = "nats-py", specifier = ">=2.12.0" },
103
- { name = "opencv-python", specifier = ">=4.12.0.88" },
104
- { name = "pyserial", specifier = "~=3.5" },
105
- ]
106
-
107
- [[package]]
108
- name = "pyserial"
109
- version = "3.5"
110
- source = { registry = "https://pypi.org/simple" }
111
- sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" }
112
- wheels = [
113
- { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" },
114
- ]
File without changes
File without changes