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-bridgebut the import package isdataray—pip install dataray-bridge, thenimport dataray. Thedatarayname 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— returnsLCM:1.1. - FPGA handshake (
0x43502B01) and the 128-byte FPGA status block (magic0x87654321, 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.
0x0Dsets 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. Usecamera.backend.auto_integration()orset_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.truncatedflags 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 near0xFFF0, not the sensor's raw 12 bits.
Other backends
simworks fully, end to end, including every CLI subcommand.- Analysis works on any numpy array, from any source.
laserlinkis 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.ocxis 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
0x02OUT — commands. An array of little-endian 32-bit words,opcode << 24 | payload(24-bit payload). Opcode0x00is 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, then0x63carrying the identical payload, which latches it. Register writes are0x64 | (index << 8) | value. - EP
0x81IN — images. Raw 16-bit little-endian pixels,width * height * 2bytes, no header. - EP
0x82IN — status / register readback. The status block is 128 bytes and starts with magic0x87654321; a register readback block carries0x5678in the high half of its first dword, and each dword isindex << 8 | value. Opcode0x03selects which of the two the next read returns. - EP
0x01OUT 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
0xE1withwValuein the range0xA0–0xAF. 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
0x02land in an FPGA FIFO instead: a misunderstood register write there is recoverable with a power cycle and cannot rewrite firmware. Every probe script inscripts/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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ec07c7bce785faad492f8c5dfff98236c559b8cecebeff69c8735561c1fce9b
|
|
| MD5 |
47fe24745fca3a5f367c3a6c829eb83b
|
|
| BLAKE2b-256 |
8127f38ec848cea3224af9849412adec19df438fc71cbeca961f1efa1e251b2e
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c91d94103bcbaedfd321a962d2a8606d4c8b4b497e8e18b102e63a9b1f558eef
|
|
| MD5 |
16dbda2abefe083a427cd967d59caba9
|
|
| BLAKE2b-256 |
2558d741a42ff2da09ed5e7f84af708c049d86fd059d6806ef71fe394eeb1318
|