Skip to main content

dataray

Native, OS-agnostic Python interface and ISO 11146 beam analysis for DataRay beam profiling cameras.

DataRay ships a Windows-only application and a 32-bit ActiveX control. This package talks to the camera's USB endpoints directly through libusb, so you need neither DataRay's software nor Windows: it runs on Linux (including aarch64 boards such as a Jetson), macOS, and Windows, on 64-bit Python, with no kernel driver and no COM.

The analysis half is independent of the hardware half. dataray.analyze() takes a plain 2-D numpy array and returns a full ISO 11146 beam profile — second-moment (D4sigma) widths, principal axes and orientation, clip-level widths, Gaussian fits — plus multi-frame statistics and an M-squared fit from a through-focus scan. That works today on frames from any source.

Dependencies are deliberately thin: numpy is the only hard requirement. pyusb, requests and scipy are opt-in extras.


Hardware supported

Camera WinCamD-LCM, USB 3.0, 1632:3000
Sensor 1" global-shutter CMOS, 2048 x 2048, 4.2 MPixel
Pixel pitch 5.5 um (11.264 x 11.264 mm active area)
Bit depth 12-bit (0..4095 counts)
Exposure 85 us .. 2 s on USB 3.0
Firmware reports LCM:1.1 (image LCM_1p1.img)

DataRay's own DRIUSB3.inf binds the WaveCamD (1632:6001) to the same USB 3.0 driver, so it should enumerate and speak the same protocol. It is not in this package's sensor table, so it is described with a generic 2048 x 2048 / 5.5 um geometry that you should check against your datasheet; it has not been tested here.


Install

The distribution is dataray-bridge but the import package is dataraypip install dataray-bridge, then import dataray. The dataray name on PyPI belongs to an unrelated 2020 package.

pip install dataray            # analysis only, numpy alone
pip install 'dataray-bridge[usb]'       # + native USB access (pyusb / libusb)
pip install 'dataray-bridge[fit]       # + scipy, for least-squares Gaussian fits
pip install 'dataray-bridge[laserlink] # + requests, for the LaserLink HTTP backend
pip install 'dataray-bridge[all]       # usb + laserlink + fit

Linux: install the udev rule

Without this, libusb cannot open the camera and you get a PermissionDeniedError. Write /etc/udev/rules.d/99-dataray.rules with exactly the content of packaging/99-dataray.rules:

# DataRay beam profiling cameras — grant userspace (libusb) access.
# WinCamD-LCM / LCM series
SUBSYSTEM=="usb", ATTR{idVendor}=="1632", MODE="0660", GROUP="plugdev", TAG+="uaccess"

Then reload and replug:

sudo cp packaging/99-dataray.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger

Your user must be in the plugdev group for the GROUP="plugdev" line to help (sudo usermod -aG plugdev "$USER", then log out and back in). The TAG+="uaccess" also grants access to the user on the local seat under systemd-logind. Unplug and replug the camera after reloading the rules.


Quickstart

Check what is attached:

dataray list
dataray --backend usb info
dataray --backend sim profile
dataray --backend sim capture -n 10 -o frames.npy
dataray --backend sim monitor --interval 0.5 --count 20

Measure a beam:

import dataray

with dataray.open_camera("sim") as cam:          # or "usb", "laserlink", "ocx"
    cam.exposure_us = 4000                        # also cam.exposure_ms
    cam.auto_exposure(target_fraction=0.65)       # settle the peak at ~65% FS

    profile = cam.capture_profile()               # capture + analyze in one call
    print(profile.centroid_x, profile.centroid_y)         # microns
    print(profile.d4sigma_x, profile.d4sigma_y)           # microns (ISO 11146)
    print(profile.d4sigma_major, profile.d4sigma_minor)   # principal axes
    print(profile.orientation)                            # degrees
    print(profile.ellipticity, profile.is_circular)
    print(profile.peak_value, profile.snr, profile.saturated_fraction)
    print(profile.width_at(0.5, "x"))             # clip-level (FWHM-equivalent)

Frame averaging and multi-frame statistics:

with dataray.open_camera("sim") as cam:
    profile = cam.capture_profile(average=8)      # sqrt(8) less read noise

    stats = cam.measure(count=50)                 # ProfileStatistics
    print(stats.d4sigma_x.mean, stats.d4sigma_x.pct_rms)   # um, % RMS
    print(stats.centroid_x.std, stats.orientation.mean)

    from dataray.analysis.statistics import centroid_stability
    print(centroid_stability(cam.capture_profiles(50)))     # jitter vs drift

Background subtraction (block the beam first):

with dataray.open_camera("sim") as cam:
    cam.acquire_background(frames=16)   # averaged dark reference
    # ... unblock the beam ...
    profile = cam.capture_profile()     # every frame is now dark-corrected
    cam.clear_background()

Analysis with no camera at all:

import numpy as np
import dataray

frame = np.load("frames_000.npy")          # anything 2-D works
profile = dataray.analyze(frame, pixel_size_um=5.5)
print(profile)

M-squared from a through-focus scan (ISO 11146):

from dataray.analysis.statistics import fit_m_squared

z_mm       = [-60, -50, -40, -30, -20, -10, 0, 10, 20, 30, 40, 50, 60]
d4sigma_um = [...]                          # one D4sigma per station
result = fit_m_squared(z_mm, d4sigma_um, wavelength_nm=1064.0)

print(result.m_squared, result.waist_diameter, result.waist_position)
print(result.rayleigh_length, result.divergence)     # mm, mrad (full angle)
print(result.zr_fit_quality)                         # R^2 of the d^2 parabola

fit_m_squared_from_profiles(z_mm, profiles, wavelength_nm, axis="major") does the same directly from measured profiles, per principal axis.

Runnable versions of all of this: examples/quickstart.py (defaults to sim, so it runs anywhere) and examples/m2_scan.py.


Backends

Select with open_camera("<name>") or dataray --backend <name>.

backend needs OS notes
usb pyusb + libusb any Native. Talks to the camera's bulk/control endpoints directly. No DataRay software. Frame streaming not working yet — see below.
sim nothing any Synthetic WinCamD-LCM: gaussian / line / donut / multi beams with shot noise, read noise, exposure response and honest saturation. Fully functional.
laserlink a Windows host running DataRay LaserLink (+ optional requests) any client HTTP+JSON to that server; set DATARAY_LASERLINK_HOST / DATARAY_LASERLINK_PORT. Untested here — we have no Windows host.
ocx Windows + DataRay's software installed + 32-bit Python Windows Drives DataRay's ActiveX controls, so results match their application exactly. Their OCX is a 32-bit in-process COM server and needs a GUI message loop; see the module docstring for the 32-bit workarounds.

list_devices() probes usb, laserlink, then ocx; the simulator is only used when you ask for it by name, so it never masks real hardware.


Status

The native usb backend captures live frames from a WinCamD-LCM. Verified against physical hardware on an aarch64 Jetson Orin with no DataRay software present:

  • Enumeration and identification (1632:3000, bus/address, string descriptors).
  • Firmware identity over vendor control read 0xB0 — returns LCM:1.1.
  • FPGA handshake (0x43502B01) and the 128-byte FPGA status block (magic 0x87654321, FPGA version 9).
  • Sensor configuration. The full 128-register file is written, then read back and compared byte for byte, with the sensor's chip ID (reg[0x7D] == 0x43) confirming the register bus really reaches the sensor.
  • Full-frame 2048x2048 image capture, 16-bit, as 8 sequential bulk transfers.
  • Integration-time control with auto_integration(), plus gain, ROI, binning and trigger configuration.

Measured on a laser sheet: FWHM across the line 303 µm, repeatable to 0.45% rms over six frames.

Caveats worth knowing

  • Exposure works through the integration time, not the frame divider. 0x0D sets the frame period; signal level comes from the sensor's signed 14-bit integration time. It is uncalibrated, so it is exposed as raw counts rather than dressed up as microseconds. Use camera.backend.auto_integration() or set_integration_time(). Camera.auto_exposure() drives the frame period and is much less effective.
  • Read frames sequentially. read_frame() does this. If you write your own transfer loop, parallel readers give no guarantee which transfer receives which part of the frame, and the image scrambles in a way that looks like sensor noise.
  • BeamProfile.truncated flags a beam that overfills the sensor. ISO 11146's second moments require containment; a clipped beam gives a D4sigma that swings wildly frame to frame (observed: 420–6250 µm) while the FWHM stays stable to under 1%. When the flag is set, use the clip widths or Gaussian fit.
  • Frames report bit_depth=16: the FPGA delivers 16-bit words and full scale is observed near 0xFFF0, not the sensor's raw 12 bits.

Other backends

  • sim works fully, end to end, including every CLI subcommand.
  • Analysis works on any numpy array, from any source.
  • laserlink is implemented against DataRay's documented REST surface and their own examples, but has never been run against a live server here; its image-decode path is written defensively for that reason.
  • ocx is implemented but only usable on Windows with DataRay's software and a 32-bit interpreter, which is not the environment this was developed on.

Protocol notes

Everything below was recovered by black-box analysis of a physical camera plus static analysis of DataRay's shipped binaries. In the source, facts are labelled CONFIRMED (observed on the device or read out of a binary) or INFERRED, because a wrong guess in a wire format yields silently wrong measurements rather than an obvious failure. See src/dataray/_proto/.

Architecture. The camera is a Cypress FX3 running a 3-channel synchronous Slave-FIFO design — a transparent DMA bridge between the USB endpoints and an FPGA. The FPGA, not the FX3, interprets camera commands. That is why the control endpoint only exposes identity and status.

Endpoints. Interface 0 is vendor-specific with four bulk endpoints (wMaxPacketSize 1024 at SuperSpeed, bMaxBurst 3):

  • EP 0x02 OUT — commands. An array of little-endian 32-bit words, opcode << 24 | payload (24-bit payload). Opcode 0x00 is a NOP/pad, so a short command may be zero-filled to a larger block. Every single-value command is emitted as two words: the command, then 0x63 carrying the identical payload, which latches it. Register writes are 0x64 | (index << 8) | value.
  • EP 0x81 IN — images. Raw 16-bit little-endian pixels, width * height * 2 bytes, no header.
  • EP 0x82 IN — status / register readback. The status block is 128 bytes and starts with magic 0x87654321; a register readback block carries 0x5678 in the high half of its first dword, and each dword is index << 8 | value. Opcode 0x03 selects which of the two the next read returns.
  • EP 0x01 OUT is enumerated and wired to a DMA channel by the firmware but never used by DataRay's software.

CLEAR_FEATURE(ENDPOINT_HALT) is implemented in firmware as a full DMA channel reset, so it is the supported way to resync a wedged pipe.

Vendor control reads. All device-to-host (bmRequestType 0xC0), fixed length, constant in wValue/wIndex: 0xB0 identity (8 ASCII bytes), 0xE2/0xE3 status, 0xE4/0xE5 alive sentinels (both 0xAA), 0xE6 config word (4 bytes), 0xE7 config short (2 bytes). 0xE0 reads the FPGA register space; with wValue = 0x0080 it returns the FPGA/PSoC identification word. This set was established by sweeping all 256 request codes across bmRequestType 0xC0/0xC1/0xC2 and 0xA0/0xA1/0xA2, and by sweeping wValue and wIndex on every responder.

Do not do this

Never issue vendor request 0xE1 with wValue in the range 0xA00xAF. On this firmware that address range is the FX3 boot EEPROM. Writing it destroys the boot image and permanently bricks the camera — it will no longer enumerate and cannot be recovered over USB.

More generally, do not send any vendor control OUT transfer while exploring. On Cypress FX3 firmware, vendor OUT codes commonly map to I2C/EEPROM writes. Bulk writes to EP 0x02 land in an FPGA FIFO instead: a misunderstood register write there is recoverable with a power cycle and cannot rewrite firmware. Every probe script in scripts/ is restricted to control IN transfers and bulk writes for exactly this reason.


Development

python -m venv .venv
.venv/bin/pip install -e '.[all,dev]'
.venv/bin/python -m pytest -q

The test suite runs entirely on synthetic beams with known ground truth and needs no hardware.


License

MIT — see LICENSE. Copyright (c) 2026 Matteo.

Disclaimer

This is an independent, unofficial project. It is not affiliated with, endorsed by, or supported by DataRay Inc. "DataRay", "WinCamD" and "LaserLink" are their names, used here only to say which hardware this talks to.

The protocol description was produced by black-box analysis of a device that was purchased outright, together with examination of publicly distributed driver and application files, for the sole purpose of interoperability — making the hardware usable from Python on operating systems the vendor does not support. No vendor source code is included or redistributed. Use it at your own risk: it can misconfigure your camera, and nothing here is calibrated or warranted for metrology you intend to rely on.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dataray_bridge-0.1.0.tar.gz (105.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dataray_bridge-0.1.0-py3-none-any.whl (84.1 kB view details)

Uploaded Python 3

File details

Details for the file dataray_bridge-0.1.0.tar.gz.

File metadata

  • Download URL: dataray_bridge-0.1.0.tar.gz
  • Upload date:
  • Size: 105.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.10.12

File hashes

Hashes for dataray_bridge-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5ec07c7bce785faad492f8c5dfff98236c559b8cecebeff69c8735561c1fce9b
MD5 47fe24745fca3a5f367c3a6c829eb83b
BLAKE2b-256 8127f38ec848cea3224af9849412adec19df438fc71cbeca961f1efa1e251b2e

See more details on using hashes here.

File details

Details for the file dataray_bridge-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: dataray_bridge-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 84.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.10.12

File hashes

Hashes for dataray_bridge-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c91d94103bcbaedfd321a962d2a8606d4c8b4b497e8e18b102e63a9b1f558eef
MD5 16dbda2abefe083a427cd967d59caba9
BLAKE2b-256 2558d741a42ff2da09ed5e7f84af708c049d86fd059d6806ef71fe394eeb1318

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page