| **Public Python wheel** | Self-serve, no credentials — the public package registry, or as files from the [releases page](https://git.byonoy.com/public/byonoy_devices_sdk/releases) |
| **Public native SDK** | Self-serve, no credentials — the [releases page](https://git.byonoy.com/public/byonoy_devices_sdk/releases) |
**Stopping a blocked measurement is a last resort.** The only way out of a call
that does not return is to end the process; the library's own internal waits
do not make it return. Ending the process works — SIGTERM or SIGKILL on Linux
and macOS, `Stop-Process` on Windows. The library gets no chance to
clean up, and the device may be left unable to measure, or may reset itself
— dropping off USB and re-enumerating a few seconds to a minute later. Plan for
a replug afterwards. In a virtual machine with USB passthrough, a device that
re-enumerates comes back to the **host**, not the VM, and has to be attached
to the VM again. If you run unattended, run each
measurement in a child process under a timeout (the Python document shows a
portable way) so that a hang costs you the measurement, not the whole program.
**The library does not check for a plate,** and no device refuses to measure
without one. Absorbance measurements on an empty slot return `NO_ERROR` and
near-zero values that mean nothing; a Luminescence 96 cannot even tell whether
a plate is inserted, and without one returns background noise around zero.
Where the device can sense the slot, checking it before measuring is your job;
the complete Python example shows how.
**A measurement returns a fixed-size result.** A 96-well read gives 96 values
whatever you selected; index `i` of the result corresponds to index `i` of your
selection. Deselecting wells does not shorten the list.
### Well order
**Results are in row-major order.** On a 96-well plate (8 rows × 12 columns),
index `i` is row `i // 12` and column `i % 12`:
| Index | 0 | 1 | … | 11 | 12 | … | 95 |
|---|---|---|---|---|---|---|---|
| Well | A1 | A2 | … | A12 | B1 | … | H12 |
```python
defwell_name(i,columns=12):
returnf"{chr(ord('A')+i//columns)}{i%columns+1}"
```
The same order applies to `selected_wells`: entry `i` selects well `i` in the
table above. It holds for every 96-well modality (absorbance, luminescence,
fluorescence). The device sends one plate row at a time and nothing between it
and your code reorders the values, so the order is fixed by the plate, not by
the readout.
- **Bottom readout needs no correction.** A device that reads the plate from
below mirrors the data itself before sending it. The result is already in
plate orientation; do not flip it again because `is_bottom_readout` is set.
### Deselected wells
A deselected well keeps its place in the result, but what it contains differs
by modality — do not treat it as a reading:
| Modality | Deselected well contains |
|---|---|
| Luminescence 96 | `0` |
| Fluorescence 96 | `NaN` |
| Absorbance 96 Automate | no well selection; every well is measured |
Because Luminescence 96 reports deselected wells as `0`, you cannot tell one
from a genuine dark reading by value. Keep your own selection and use it to
filter the result.
### Units
| Modality | Unit | Meaning |
|---|---|---|
| Absorbance (Absorbance 96 Automate, Absorbance One) | OD — optical density | how much light the sample absorbs, on a log₁₀ scale: 0 lets all light through, 1 lets 10 % through, 2 lets 1 % through |
| Luminescence | RLU — relative light units | how much light the sample emits, on the instrument's own scale |
| Fluorescence | RFU — relative fluorescence units | how much light the sample emits when excited, on the instrument's own scale |
RLU and RFU are not calibrated physical units: compare readings taken on the
same device, in the same mode (and for fluorescence, the same filter set),
rather than across devices or settings.
### Device status and device error
Two calls describe the device's own health, separately from the return code of
any single call:
**`get_device_status`** asks the device and gives a state:
| State | Meaning |
|---|---|
| `OK` | the device reports no error |
| `ERROR` | the device reports an error — see `get_device_error` |
| `BROKEN_FW` | the firmware is corrupted or unidentifiable; the device needs a firmware update |
| `UNKNOWN` | the device did not answer the status request |
Anything but `OK` means: do not measure.
**`get_device_error`** gives the last error recorded for the device, from a
small set shared by all devices. It does not ask the device itself — call
`get_device_status` first for a current value.
| Device error | Meaning |
|---|---|
| `0` | no error |
| `0x8001` (32769) | the device reported an error of its own — the common case; see below |
| `0x8004` (32772) | the device did not respond |
| `0x8009` (32777) | the device was disconnected |
| `0x8000` (32768) | unknown error |
These numbers are **not** the library's return codes listed under
[Error codes worth recognising](#error-codes-worth-recognising), even where
they coincide (`0x8001` there is `MEASUREMENT_SLOT_NOT_EMPTY`).
**What the device actually reported** is not available through the API. It
For the luminescence and fluorescence devices the log shows only the raw
firmware code, as `errorCode=0x…`. On a **Luminescence 96** it is a single
value:
| Code | Meaning | Cleared by |
|---|---|---|
| `0x1` | calibration missing — the detector calibration could not be loaded | a power cycle; report it |
| `0x7` | shutter error — the shutter failed to move after repeated retries during a measurement | a power cycle |
On a **Fluorescence 96** it is a set of flags, several of which can be set at
once:
| Bit | Value | Meaning |
|---|---|---|
| 0 | `0x01` | calibration missing |
| 1 | `0x02` | configuration — the device's identity or board revision could not be read |
| 2 | `0x04` | homing — the slide is not homed; check that it moves freely and the transport lock is out |
| 3 | `0x08` | detector — usually the detector sled's supply or connection rather than the photodiodes |
| 4 | `0x10` | hall position — the position sensor does not answer |
| 5 | `0x20` | accelerometer — the accelerometer does not answer |
| 6 | `0x40` | photodiode compensation — no excitation light seen |
| 7 | `0x80` | motion system — the slide stalled, or homing never found its endstop |
A Fluorescence 96 clears all flags at the start of every measurement and on a
restart, so a code seen after a failed run describes that run. Any flag
aborts a running measurement. Report codes to Byonoy support with the serial
number and firmware version.
`ERROR` clears by itself once the device stops reporting the condition; no
reconnect is needed. A device that keeps reporting one — such as an Absorbance
One with a dirty optical path — stays in `ERROR` across closing, reopening and
new processes. It still answers queries, but its measurements fail or hang.
The opposite case — status `OK`, error `0`, yet every measurement fails — is
described under [When the device stops measuring](#when-the-device-stops-measuring).
### Error codes worth recognising
The library defines 25 error codes; these are the ones you are most likely to
meet. Print the name and number (`rc.name`, `hex(rc.value)` in Python) rather
than assuming an unlisted value is impossible.
| Code | Name | Usual cause |
|---|---|---|
| `0x0000` | `NO_ERROR` | success |
| `0x0002` | `DEVICE_CLOSED` | device unplugged or lost while open |
| `0x0003` | `INVALID_ARGUMENT` | a handle you already freed, a config you did not create, or a value the device does not accept (see also the Absorbance 96 initialise note in the Python document) |
| `0x0005` | `UNSUPPORTED_OPERATION` | wrong modality or feature for this device |
| `0x0006` | `DEVICE_COMMUNICATION_FAILURE` | another process holds the device; on Linux, missing permissions; on macOS, nobody logged in to a desktop session; otherwise cable, power, or a device mid-reset |
| `0x0007` | `DEVICE_OPERATION_FAILED` | the device refused the operation. If the device status is `ERROR`, that is the cause; if it is `OK` and every measurement fails this way, see [When the device stops measuring](#when-the-device-stops-measuring) |
| `0x0104` | `DEVICE_ALREADY_OPEN` | your own process already has it open |
| `0x8001` | `MEASUREMENT_SLOT_NOT_EMPTY` | absorbance: initialising with a plate or cuvette in the slot |
| `0x8002` | `NOT_INITIALIZED` | absorbance: measuring a wavelength that was not initialised **on this handle** — initialisation does not survive closing the device |
| `0x8003` | `MEASUREMENT_PARTS_NOT_ALIGNED` | the device's parts are not seated correctly |
### Linux permissions
USB access is root-only by default. Without a udev rule the device still
appears in `available_devices()`, but with an **empty serial number**, and
opening it fails with `DEVICE_COMMUNICATION_FAILURE` (`0x0006`). Rather than
running as root, install the suggested rules in
**[`70-byonoy.rules`](70-byonoy.rules)** — the same rules the Byonoy desktop
app installs. The file sits next to this guide; copy it onto the machine, then:
> **Report it if it recurs.** A one-off on an engineering or pre-production unit
> is not remarkable. A device that needs rebooting repeatedly, or one on which a
> particular mode fails consistently, is a fault worth reporting to Byonoy
> support with the serial number, the firmware version from the device
> information call, the mode and well selection you used, and how often it
> happens. Do not work around it with a reboot loop — that hides a hardware
> problem behind software.
---
## 4. Next
- **[Python SDK →](SDK_PYTHON.md)** — including a [complete measure-to-CSV program](SDK_PYTHON.md#6-complete-example-measure-and-export-csv) covering every device