Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/_static/devices.json
Original file line number Diff line number Diff line change
Expand Up @@ -1208,6 +1208,20 @@
"manager": "https://discuss.pylabrobot.org/u/rickwierenga",
"oem": "https://lifesciences.tecan.com/multimode-plate-reader"
},
{
"id": "thermo-fisher-nanodrop-1000",
"vendor": "Thermo Fisher",
"name": "NanoDrop 1000",
"kind": "plate reader",
"capabilities": [
"absorbance"
],
"status": "basic",
"api": "pylabrobot.thermo_fisher.nanodrop_1000.ThermoFisherNanoDrop1000",
"api_version": "v1",
"code_slug": "thermo_fisher/nanodrop_1000",
"doc_slug": "thermo_fisher/nanodrop_1000/index"
},
{
"id": "thermo-fisher-alps-300",
"vendor": "Thermo Fisher",
Expand Down
1 change: 1 addition & 0 deletions docs/user_guide/thermo_fisher/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
:maxdepth: 1

alps/index
nanodrop_1000/index
```
79 changes: 79 additions & 0 deletions docs/user_guide/thermo_fisher/nanodrop_1000/hello-world.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "nanodrop-intro",
"metadata": {},
"source": "# Thermo Fisher NanoDrop 1000\n\nThe NanoDrop 1000 measures absorbance spectra from small-volume samples. Complete the [NanoDrop 1000 setup guide](setup.md) before connecting with PyLabRobot."
},
{
"cell_type": "markdown",
"id": "nanodrop-connect-heading",
"metadata": {},
"source": "## Connect"
},
{
"cell_type": "code",
"execution_count": null,
"id": "nanodrop-connect",
"metadata": {},
"outputs": [],
"source": "from pylabrobot.thermo_fisher.nanodrop_1000 import ThermoFisherNanoDrop1000\n\nnanodrop = ThermoFisherNanoDrop1000()\nawait nanodrop.setup()"
},
{
"cell_type": "markdown",
"id": "nanodrop-blank-heading",
"metadata": {},
"source": "## Blank\n\nClean the pedestals, load the blank solution, lower the sampling arm, and acquire the blank."
},
{
"cell_type": "code",
"execution_count": null,
"id": "nanodrop-blank",
"metadata": {},
"outputs": [],
"source": "await nanodrop.take_blank(integration_ms=20)"
},
{
"cell_type": "markdown",
"id": "nanodrop-measure-heading",
"metadata": {},
"source": "## Measure a sample\n\nClean the pedestals, load the sample, lower the sampling arm, and measure absorbance. The result contains matching wavelength and absorbance lists."
},
{
"cell_type": "code",
"execution_count": null,
"id": "nanodrop-measure",
"metadata": {},
"outputs": [],
"source": "wavelengths, absorbance = await nanodrop.measure_absorbance(integration_ms=20)\n\npeak_index = max(range(len(absorbance)), key=absorbance.__getitem__)\nprint(f\"Peak absorbance: {absorbance[peak_index]:.3f} at {wavelengths[peak_index]:.1f} nm\")"
},
{
"cell_type": "markdown",
"id": "nanodrop-stop-heading",
"metadata": {},
"source": "## Disconnect"
},
{
"cell_type": "code",
"execution_count": null,
"id": "nanodrop-stop",
"metadata": {},
"outputs": [],
"source": "await nanodrop.stop()"
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.11.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
15 changes: 15 additions & 0 deletions docs/user_guide/thermo_fisher/nanodrop_1000/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Thermo Fisher NanoDrop 1000

```{device-card} thermo-fisher-nanodrop-1000
```

The NanoDrop 1000 measures absorbance spectra from small-volume samples. Start with the setup guide
to configure the USB connection, then follow the hello-world example to blank the instrument and
measure a sample.

```{toctree}
:maxdepth: 1

setup
hello-world
```
49 changes: 49 additions & 0 deletions docs/user_guide/thermo_fisher/nanodrop_1000/setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# NanoDrop 1000 setup

Install PyLabRobot with USB support:

```bash
pip install "pylabrobot[usb]"
```

## Windows USB driver

The NanoDrop 1000 must use a libusb-compatible driver before PyLabRobot can communicate with it.
Changing this driver prevents the official NanoDrop software from using the device until you switch
the driver back.

Some Windows 10 and 11 systems report the NanoDrop as an unknown USB device because its older USB
controller does not provide a BOS descriptor. Only if Device Manager reports that problem, open an
administrator Command Prompt and run:

```bat
reg add "HKLM\SYSTEM\CurrentControlSet\Control\usbflags\245710020002" /v SkipBOSDescriptorQuery /t REG_DWORD /d 1 /f
```

Unplug and reconnect the NanoDrop after changing the setting.

Install the libusb driver with Zadig:

1. Download and run [Zadig](https://zadig.akeo.ie/).
2. Select **Options > List All Devices**.
3. Select **NanoDrop 1000**.
4. Select **libusb-win32** as the replacement driver.
5. Click **Replace Driver** or **Install Driver**.

## Restore the official driver

To use the official NanoDrop software again:

1. Open Device Manager.
2. Find the NanoDrop 1000 under **libusb-win32 devices**.
3. Select **Update driver > Browse my computer for drivers > Let me pick from a list**.
4. Select the original NanoDrop or Cypress EZ-USB driver.

If you added the BOS descriptor registry setting and want to remove it, run this from an
administrator Command Prompt:

```bat
reg delete "HKLM\SYSTEM\CurrentControlSet\Control\usbflags\245710020002" /v SkipBOSDescriptorQuery /f
```

Continue with the [NanoDrop 1000 hello world](hello-world.md).
49 changes: 46 additions & 3 deletions pylabrobot/io/usb.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,14 +228,20 @@ def _read_packet(
# No data available (yet), this will give a timeout error. Don't reraise.
return None

async def read(self, timeout: Optional[int] = None, size: Optional[int] = None) -> bytes:
async def read(
self,
timeout: Optional[int] = None,
size: Optional[int] = None,
endpoint: Optional[int] = None,
) -> bytes:
"""Read a response from the device.

Args:
timeout: The timeout for reading from the device in seconds. If `None`, use the default
timeout (specified by the `read_timeout` attribute).
size: The maximum number of bytes to read. If `None`, read all available data until no
more packets arrive.
endpoint: The endpoint address to read from. If `None`, use the configured read endpoint.
"""

if self.dev is None or self.read_endpoint is None:
Expand All @@ -255,7 +261,7 @@ def read_or_timeout() -> bytes:
last_packet: Optional[bytearray] = None
while True: # read while we have data, and while the last packet is the max size.
remaining = size - len(resp) if size is not None else None
last_packet = self._read_packet(size=remaining)
last_packet = self._read_packet(size=remaining, endpoint=endpoint)
if last_packet is not None:
resp += last_packet
if self.read_endpoint is None:
Expand Down Expand Up @@ -296,6 +302,29 @@ def read_or_timeout() -> bytes:
)
return response

async def drain(
self,
endpoint: Optional[int] = None,
timeout: float = 0.05,
size: int = 512,
max_duration: float = 1,
) -> None:
"""Discard queued input from an endpoint until no packet arrives before the timeout."""
if self.dev is None or self.read_endpoint is None:
raise RuntimeError(f"USB device for '{self.human_readable_device_name}' is not connected.")

deadline = time.monotonic() + max_duration
loop = asyncio.get_running_loop()
while time.monotonic() < deadline:
packet = await loop.run_in_executor(
self.read_executor,
lambda: self._read_packet(size=size, timeout=timeout, endpoint=endpoint),
)
if packet is None:
return

raise TimeoutError(f"Timed out draining USB device '{self.human_readable_device_name}'.")

def get_available_devices(self) -> List["usb.core.Device"]:
"""Get a list of available devices that match the specified vendor and product IDs, and serial
number and device_address if specified."""
Expand Down Expand Up @@ -561,7 +590,12 @@ async def write(self, data: bytes, timeout: Optional[float] = None):
align_sequences(expected=next_command.data, actual=decoded)
raise ValidationError("Data mismatch: difference was written to stdout.")

async def read(self, timeout: Optional[float] = None, size: Optional[int] = None) -> bytes:
async def read(
self,
timeout: Optional[float] = None,
size: Optional[int] = None,
endpoint: Optional[int] = None,
) -> bytes:
next_command = USBCommand(**self.cr.next_command())
if not (
next_command.module == "usb"
Expand All @@ -574,6 +608,15 @@ async def read(self, timeout: Optional[float] = None, size: Optional[int] = None
data = data[:size]
return data

async def drain(
self,
endpoint: Optional[int] = None,
timeout: float = 0.05,
size: int = 512,
max_duration: float = 1,
) -> None:
pass

def ctrl_transfer(
self,
bmRequestType: int,
Expand Down
3 changes: 3 additions & 0 deletions pylabrobot/thermo_fisher/nanodrop_1000/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .nanodrop_1000 import ThermoFisherNanoDrop1000

__all__ = ["ThermoFisherNanoDrop1000"]
Loading