Nanodrop (nanodrop v0.3.1)

Copy Markdown

Elixir library for interfacing with NanoDrop 1000 spectrophotometers.

The NanoDrop 1000 internally uses an Ocean Optics USB2000 spectrometer and communicates via the OOI (Ocean Optics Interface) protocol over USB.

Quick Start

# Start the server (connects to first available device)
{:ok, pid} = Nanodrop.start_link()

# Calibrate with water/buffer on pedestal (takes dark + blank in one shot)
:ok = Nanodrop.calibrate(pid)

# Measure a sample (includes full spectrum in result)
{:ok, result} = Nanodrop.measure_nucleic_acid(pid)
# => %{a260: 1.5, a280: 0.75, a260_a280: 2.0, concentration_ng_ul: 75.0, spectrum: ...}

# Or get the spectrum and analyze it
{:ok, spectrum} = Nanodrop.get_spectrum(pid)
abs_280 = Nanodrop.absorbance_at(spectrum, 280.0)

Device Identification

  • USB Vendor ID: 0x2457 (Ocean Optics)
  • USB Product ID: 0x1002 (USB2000)

Calibration

For accurate absorbance measurements, you need two reference spectra:

  1. Dark - Detector baseline with no light (close pedestal arm, no sample)
  2. Blank - 100% transmission through solvent (water/buffer on pedestal)

Absorbance is then calculated as: A = -log10((sample - dark) / (blank - dark))

Distributed Operation

This module is designed to work over Erlang distribution. The NanoDrop server can run on a dedicated node (e.g., a Nerves device with USB access) while being controlled from a remote node.

Direct node reference

# On the device node (e.g., nanodrop@device.local)
{:ok, pid} = Nanodrop.start_link(name: Nanodrop)

# From a remote node
Nanodrop.set_dark({Nanodrop, :"nanodrop@device.local"})
Nanodrop.set_blank({Nanodrop, :"nanodrop@device.local"})
{:ok, result} = Nanodrop.measure_nucleic_acid({Nanodrop, :"nanodrop@device.local"})

Global registration

# On the device node
{:ok, pid} = Nanodrop.start_link(name: {:global, :nanodrop})

# From any connected node
Nanodrop.set_dark({:global, :nanodrop})
{:ok, result} = Nanodrop.measure_nucleic_acid({:global, :nanodrop})

Process groups (pg)

# On the device node
{:ok, pid} = Nanodrop.start_link()
:pg.join(:spectrophotometers, pid)

# From any connected node
[pid | _] = :pg.get_members(:spectrophotometers)
{:ok, result} = Nanodrop.measure_nucleic_acid(pid)

All API functions accept any valid GenServer.server() reference.

Summary

Functions

Returns the absorbance at a specific wavelength from a spectrum.

Performs full calibration in one shot (dark + blank).

Returns whether the device is calibrated (has both dark and blank spectra).

Returns a specification to start this module under a supervisor.

Acquires a raw spectrum from the device.

Acquires a spectrum and calculates absorbance values.

Returns device information.

Lists all connected NanoDrop devices.

Measures nucleic acid concentration.

Measures protein concentration using A280.

Returns the device serial number.

Measures and stores the blank/reference spectrum.

Measures and stores the dark spectrum.

Sets the integration time in microseconds.

Starts the NanoDrop server and connects to a device.

Returns the wavelength calibration coefficients.

Types

absorbance_spectrum()

@type absorbance_spectrum() :: %{
  absorbance: [float()],
  wavelengths: [float()],
  timestamp: DateTime.t()
}

wavelength_calibration()

@type wavelength_calibration() :: %{
  intercept: float(),
  first_coefficient: float(),
  second_coefficient: float(),
  third_coefficient: float()
}

Functions

absorbance_at(spectrum, wavelength_nm)

@spec absorbance_at(absorbance_spectrum(), float()) :: float()

Returns the absorbance at a specific wavelength from a spectrum.

Wavelength is in nanometers. This is a pure function that operates on a spectrum returned by get_spectrum/1.

calibrate(server)

@spec calibrate(GenServer.server()) :: :ok | {:error, term()}

Performs full calibration in one shot (dark + blank).

Call this with your reference solvent (water, buffer) on the pedestal and the arm closed. This will:

  1. Take a dark measurement (no lamp flash) - detector baseline
  2. Take a blank measurement (lamp flashes) - reference through solvent

This is more efficient than calling set_dark/1 and set_blank/1 separately since both measurements are taken with the same sample in place.

calibrated?(server)

@spec calibrated?(GenServer.server()) :: boolean()

Returns whether the device is calibrated (has both dark and blank spectra).

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

get_raw_spectrum(server)

@spec get_raw_spectrum(GenServer.server()) ::
  {:ok, Nanodrop.Spectrum.t()} | {:error, term()}

Acquires a raw spectrum from the device.

Returns pixel intensity values without any processing.

get_spectrum(server)

@spec get_spectrum(GenServer.server()) ::
  {:ok, absorbance_spectrum()} | {:error, term()}

Acquires a spectrum and calculates absorbance values.

Requires calibration (dark and blank spectra). Returns a spectrum with absorbance and wavelength values for each pixel.

This is the single GenServer call for spectrum acquisition. Use this with absorbance_at/2 or the measurement functions for analysis.

info(server)

@spec info(GenServer.server()) :: map()

Returns device information.

list_devices()

@spec list_devices() :: [Nanodrop.Device.device_info()]

Lists all connected NanoDrop devices.

Returns a list of device info maps that can be used with start_link/1.

Example

Nanodrop.list_devices()
#=> [%{vendor_id: 9303, product_id: 4098, bus: 1, address: 19, device_ref: #Reference<...>}]

measure_nucleic_acid(server, opts \\ [])

@spec measure_nucleic_acid(
  GenServer.server(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Measures nucleic acid concentration.

Returns A260, A280, A260/A280 ratio, estimated concentration, and the full spectrum. Uses the approximation: 1 A260 = 50 ng/µL for dsDNA (1mm path).

Options

  • :factor - Conversion factor (default: 50.0 for dsDNA, use 33.0 for ssDNA, 40.0 for RNA)

Requires calibration.

measure_protein(server, opts \\ [])

@spec measure_protein(
  GenServer.server(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Measures protein concentration using A280.

Returns A280, estimated concentration, and the full spectrum.

Options

  • :extinction_coefficient - Extinction coefficient (default: 1.0, meaning 1 A280 = 1 mg/mL)

Requires calibration.

serial_number(server)

@spec serial_number(GenServer.server()) :: String.t()

Returns the device serial number.

set_blank(server)

@spec set_blank(GenServer.server()) :: :ok | {:error, term()}

Measures and stores the blank/reference spectrum.

Call this with your reference solvent (water, buffer) on the pedestal.

set_dark(server)

@spec set_dark(GenServer.server()) :: :ok | {:error, term()}

Measures and stores the dark spectrum.

Call this with the light path blocked (pedestal arm closed, nothing on pedestal).

set_integration_time(server, microseconds)

@spec set_integration_time(GenServer.server(), pos_integer()) ::
  :ok | {:error, term()}

Sets the integration time in microseconds.

Valid range: 3,000 - 655,350,000 µs. Default is 100,000 µs (100ms).

start_link(opts \\ [])

@spec start_link(keyword()) :: GenServer.on_start()

Starts the NanoDrop server and connects to a device.

Returns :ignore if running in network-only mode or if the USB library is not available. This allows the application to start on nodes that don't have USB access (e.g., remote control nodes in a distributed setup).

Options

  • :device - A device info map from Nanodrop.list_devices/0. If not provided, connects to the first available device.
  • :name - Optional name for the GenServer.

Configuration

  • :network_only - When set to true in application config, the server will return :ignore instead of connecting to USB. This is useful for nodes that only need to call a remote NanoDrop server over distribution.

    config :nanodrop, network_only: true

wavelength_calibration(server)

@spec wavelength_calibration(GenServer.server()) :: map()

Returns the wavelength calibration coefficients.