488 lines
24 KiB
Markdown
488 lines
24 KiB
Markdown
# Byonoy Device Library — SDK guide
|
||
|
||
How to obtain and safely use the Byonoy device SDKs. Start here, then follow the
|
||
link for the form you need:
|
||
|
||
- **[Python SDK](SDK_PYTHON.md)** — a wheel you `import`. Scripting, tests, automation.
|
||
- **[Native SDK](SDK_NATIVE.md)** — headers and a shared library. C, C++, or any FFI.
|
||
|
||
Both drive the same library and the same devices.
|
||
|
||
These documents are deliberately explicit: they spell out defaults, return
|
||
conventions and failure modes that are easy to get wrong, so that they can be
|
||
followed without any other context — whether you are a developer integrating a
|
||
device or a coding agent writing code against it. If something you need is not
|
||
covered here, ask Byonoy rather than inferring it.
|
||
|
||
> [!IMPORTANT]
|
||
> **Manual Absorbance 96: use the serial protocol, not this SDK.** The manual
|
||
> variant of Absorbance 96 is driven **only** through a plain-text serial
|
||
> protocol, documented publicly in
|
||
> **[abs96serial](https://git.byonoy.com/public/abs96serial)**; the SDK does not
|
||
> support it. That protocol is used by no other Byonoy device — the Absorbance
|
||
> 96 Automate and every other device are driven through the SDK described here.
|
||
|
||
---
|
||
|
||
## 1. Which variant: public or internal
|
||
|
||
The library ships in two variants.
|
||
|
||
**The public variant is what is generally available outside Byonoy, and it is
|
||
enough for everyday operation.** It covers the whole normal working life of an
|
||
instrument:
|
||
|
||
- discovering connected devices and opening them
|
||
- device information, status, errors, temperature, humidity, uptime, slot and
|
||
alignment state
|
||
- every measurement modality the hardware offers — absorbance (Absorbance One
|
||
and Absorbance 96 Automate), luminescence, fluorescence
|
||
- firmware update
|
||
|
||
If your job is "talk to the instrument and take readings", the public variant is
|
||
the right answer and the rest of this section does not apply to you.
|
||
|
||
**The internal variant is available on request.** Ask Byonoy for it if you run
|
||
into a specific limitation the public variant cannot express — not as a default
|
||
choice. It is a narrower, less travelled path, and its extra surface can change
|
||
without the consideration given to the public API.
|
||
|
||
What it adds is described where it is relevant: see the internal chapter of the
|
||
[Python](SDK_PYTHON.md) or [Native](SDK_NATIVE.md) document.
|
||
|
||
---
|
||
|
||
## 2. How to get it
|
||
|
||
| What | How |
|
||
|---|---|
|
||
| **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) |
|
||
| **Internal Python wheel** | On request from Byonoy; installed from the internal registry with a token |
|
||
| **Internal native SDK** | On request from Byonoy |
|
||
|
||
Everything public is self-serve: each release on the
|
||
[releases page](https://git.byonoy.com/public/byonoy_devices_sdk/releases) carries the native SDK archive, every Python wheel and
|
||
a `SHA256SUMS` file with their checksums. The internal variant is never
|
||
published there.
|
||
|
||
The per-form documents cover each of these in detail.
|
||
|
||
### Credentials
|
||
|
||
Nothing you install anonymously needs a token. For anything Byonoy provides from
|
||
the internal registry you will be given a `git.byonoy.com` account or token; the
|
||
per-form documents show where it goes. Export it as:
|
||
|
||
```bash
|
||
export BYONOY_USER='<your gitea username>'
|
||
export BYONOY_TOKEN='<your gitea token>'
|
||
```
|
||
|
||
```powershell
|
||
$env:BYONOY_USER='<your gitea username>'
|
||
$env:BYONOY_TOKEN='<your gitea token>'
|
||
```
|
||
|
||
Commands in these documents are written for **bash** (Linux, macOS). Where
|
||
Windows differs, the Python document gives the PowerShell equivalent.
|
||
|
||
---
|
||
|
||
## 3. How the library works
|
||
|
||
These apply to both forms and both variants. The per-form documents show the
|
||
code; this is the behaviour behind it.
|
||
|
||
**A device must be physically attached over USB.** There is no simulator. An
|
||
empty device list is the normal answer when nothing is plugged in, not an error.
|
||
For prototyping in Python, a small mock can stand in for a device — see
|
||
[Prototyping without a device](SDK_PYTHON.md#7-prototyping-without-a-device).
|
||
|
||
**The lifecycle is discover → open → use → close.** Opening gives you a handle;
|
||
everything else takes that handle; closing invalidates it. Using a closed handle
|
||
returns `INVALID_ARGUMENT`, not `DEVICE_CLOSED`.
|
||
|
||
**Check every return code.** Almost every call reports one. A value handed back
|
||
alongside a non-`NO_ERROR` code is meaningless — and it is not empty: a failed
|
||
96-well measurement still hands back 96 zeros, so testing the result's length
|
||
or truthiness does not tell you it worked.
|
||
|
||
**Ask what the device supports before asking it to do something.** Each
|
||
modality and feature has a `*_supported` predicate; calling something the device
|
||
does not support gives `UNSUPPORTED_OPERATION`. Do not branch on the model name.
|
||
|
||
- **Decide the modality from the `*_measurement_supported` predicates only.**
|
||
Helper predicates can answer `True` on a device that cannot perform the
|
||
modality: an Absorbance One reports `abs96_available_wavelengths_supported`
|
||
as `True`, and an Absorbance 96 Automate reports
|
||
`absone_available_wavelength_supported` as `True`, while the corresponding
|
||
`*_measurement_supported` is `False`.
|
||
- `abs96_modules_supported`, `abs96_get_modules` and `abs96_setup_modules` concern
|
||
the Absorbance 96's wavelength modules and are not needed for measuring. Do
|
||
not call `abs96_setup_modules` unless Byonoy asks you to.
|
||
- **Discovery and device information can name different types.** Discovery
|
||
(`available_devices()`) reports the type from the USB IDs alone, and every
|
||
Absorbance One is reported there as `AbsorbanceOneOr96`. Once the device is
|
||
open, `get_device_information()` reports what it actually is
|
||
(`AbsorbanceOne`). Treat the latter as authoritative.
|
||
|
||
**One open handle per device.** What happens when you try a second one
|
||
depends on who holds the first:
|
||
|
||
- another handle in **your own process** → `DEVICE_ALREADY_OPEN` (`0x0104`)
|
||
- the device held by **another process**, on Linux and macOS →
|
||
`DEVICE_COMMUNICATION_FAILURE` (`0x0006`)
|
||
- the device held by **another process, on Windows** → the second open
|
||
**succeeds**, and both processes can talk to the device at once. Nothing
|
||
stops them interleaving commands. On Windows, making sure only one program
|
||
uses a device is your job.
|
||
|
||
`0x0006` does not name its cause. Before treating it as a hardware fault, check
|
||
the usual ones: another program — a stray REPL, a previous run, another tool —
|
||
still has the device open; on Linux, the device is not accessible to your user
|
||
(see [Linux permissions](#linux-permissions)); on macOS, nobody is logged in at
|
||
the machine (see below).
|
||
|
||
**Measurements block, and can take a long time.** Ctrl-C will not interrupt a
|
||
measurement in progress — SIGINT is ignored while the call is inside the
|
||
library. Size timeouts from the mode you actually use.
|
||
|
||
The luminescence and fluorescence modes set an integration time:
|
||
`RAPID` 100 ms, `SENSITIVE` 2 s, `ULTRA_SENSITIVE` 20 s, or
|
||
`custom_integration_time_ms` for `CUSTOM` (at least 50). How that turns into a
|
||
duration differs between the two devices.
|
||
|
||
**Luminescence 96** reads the plate through 16 detector channels of 6 wells
|
||
each, one channel after another, and spends the integration time — but at
|
||
least 300 ms — on each channel in use. With the device's sampling overhead a
|
||
run takes roughly *(channels used) × max(integration time, 300 ms) × 1.9*, so a
|
||
full plate is about 30 × the integration time, and `RAPID` is limited by the
|
||
300 ms floor. A channel counts as used when any of its 6 wells is selected;
|
||
which wells share a channel is fixed by the hardware, so selecting fewer wells
|
||
saves time only when it frees whole channels:
|
||
|
||
| Mode | Integration time | Full plate |
|
||
|---|---|---|
|
||
| `RAPID` | 100 ms | ~10 s |
|
||
| `SENSITIVE` | 2 s | ~60 s |
|
||
| `ULTRA_SENSITIVE` | 20 s | **~10 min** |
|
||
| `CUSTOM` | `custom_integration_time_ms`, at least 50 | ~30 × that, plus overhead |
|
||
|
||
**Fluorescence 96** steps across the plate one column at a time and measures
|
||
every column that contains **at least one selected well**; columns with no
|
||
selected well are skipped entirely. A run therefore takes roughly
|
||
*(number of columns used) × (time per column)*, plus a short start-up, and the
|
||
time per column grows with the integration time. Deselecting wells within a
|
||
column that is measured anyway saves nothing — to shorten a run, leave out
|
||
whole columns. The exact time per column depends on the device's calibration,
|
||
so it differs between units.
|
||
|
||
**Absorbance:**
|
||
|
||
| Device | Call | Duration |
|
||
|---|---|---|
|
||
| Absorbance 96 Automate | initialise, one wavelength | 1.3–2 s (~2.7 s with a reference wavelength) |
|
||
| Absorbance 96 Automate | initialise, two wavelengths | ~3 s |
|
||
| Absorbance 96 Automate | measure, one wavelength | ~1.2 s (~0.7 s in rapid mode) |
|
||
| Absorbance 96 Automate | measure, two wavelengths | ~2.8 s |
|
||
| Absorbance One | measure | under 5 s |
|
||
|
||
- **Do not stop a long measurement early.** An `ULTRA_SENSITIVE` read of a full
|
||
plate takes about 10 minutes. On a test unit, one that was ended after 9
|
||
minutes left the device unable to measure until it was power-cycled (see
|
||
[When the device stops measuring](#when-the-device-stops-measuring)). If
|
||
10 minutes per read is too long for you, use `SENSITIVE` or select fewer
|
||
wells.
|
||
- **`CUSTOM` is rejected below 50 ms** (`INVALID_ARGUMENT`). There is no upper
|
||
limit — the device splits a long integration into shorter steps — and the
|
||
duration follows the formulas above.
|
||
- **A measurement can block indefinitely instead of failing.** On an Absorbance
|
||
One in an error state, `absone_measure` never returned — without an
|
||
initialise it did **not** return `NOT_INITIALIZED` — whether or not
|
||
initialisation had been attempted. Treat it as a rule: call `absone_measure`
|
||
only when the device status is `OK` and `absone_is_initialized` reports
|
||
`True` (see [Device status and device error](#device-status-and-device-error)).
|
||
|
||
**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
|
||
def well_name(i, columns=12):
|
||
return f"{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
|
||
appears only in the protocol log (`enable_logging`), as a text ID such as
|
||
`com.byonoy-AbsOne-MIN_LIGHT_ERROR`. For the two absorbance devices, these mean:
|
||
|
||
| Text ID | Meaning |
|
||
|---|---|
|
||
| `AbsOne-AMBIENT_LIGHT_ERROR` | too much ambient light is reaching the device, or it is defective |
|
||
| `AbsOne-MIN_LIGHT_ERROR` | too little light: the device is dirty, the slot is occupied, or it is defective |
|
||
| `AbsOne-TIMEOUT_ERROR` | the measurement was disrupted, e.g. by a shadow in the device |
|
||
| `AbsOne-NOISE_LIMIT_ERROR` | USB cable or hub problem, or a defective device |
|
||
| `AbsOne-HARDWARE_ERROR`, `AbsOne-UNRECOVERABLE_ERROR` | the device is defective; nothing the user can do |
|
||
| Absorbance 96: calibration failed | initialise failed — usually a plate in the slot or a dirty slot |
|
||
| Absorbance 96: ambient light | too much light is entering the device |
|
||
| Absorbance 96: USB power | insufficient USB power — use another port or a powered hub |
|
||
| Absorbance 96: temperature | the device temperature is outside its specified range |
|
||
| Absorbance 96: measurement unit | no contact with the upper section — make sure the device parts are aligned and seated |
|
||
| Absorbance 96: hardware | hardware defect — contact support |
|
||
|
||
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) |
|
||
| `0x0101` | `DEVICE_NOT_FOUND` | nothing matching attached |
|
||
| `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:
|
||
|
||
```bash
|
||
sudo install -m 0644 70-byonoy.rules /etc/udev/rules.d/70-byonoy.rules
|
||
sudo udevadm control --reload-rules
|
||
sudo udevadm trigger --subsystem-match=usb --subsystem-match=hidraw --subsystem-match=tty
|
||
sudo usermod -aG dialout "$USER" # needed for SSH, services and CI; then log in again
|
||
```
|
||
|
||
Check that it took effect, in a new login:
|
||
|
||
```bash
|
||
id | grep -o dialout # your account is in the group
|
||
lsusb -d 16d0: # e.g. "Bus 003 Device 006: ID 16d0:106a …"
|
||
ls -l /dev/bus/usb/003/006 # the Bus/Device numbers from lsusb: group dialout
|
||
```
|
||
|
||
The library talks to the device through libusb, so the USB node under
|
||
`/dev/bus/usb` is the one whose permissions matter. The device's
|
||
`/dev/hidraw*` node is not a reliable check: it disappears while a program has
|
||
the device open, and can stay missing after a program was killed even though
|
||
the device works. If the USB node's group is not `dialout`, unplug the device
|
||
and plug it back in.
|
||
|
||
An empty serial number is not always a permissions problem. After a process
|
||
was ended in the middle of a measurement, a device can be listed with an empty
|
||
serial number while opening it returns `DEVICE_COMMUNICATION_FAILURE`
|
||
(`0x0006`) or `DEVICE_NOT_FOUND` (`0x0101`) — typically because it is resetting
|
||
and about to drop off USB and re-enumerate. If permissions were fine before,
|
||
wait a minute and look again; if it does not come back, replug it.
|
||
|
||
The rules cover Byonoy devices by USB vendor and product ID (vendor `16d0`,
|
||
plus `0483:ab12`), for both the raw USB and the `hidraw` device nodes. They
|
||
grant access in two ways:
|
||
|
||
- `TAG+="uaccess"` gives the user logged in at the machine's own seat access
|
||
automatically. This does **not** apply to SSH sessions, containers, CI
|
||
runners or services.
|
||
- `GROUP="dialout"` gives access to members of the `dialout` group — the
|
||
`usermod` line above. Anything that is not a local desktop session depends on
|
||
it.
|
||
|
||
The last two lines match the manual Absorbance 96's serial adapter (vendor
|
||
`0403`, manufacturer `Byonoy GmbH`) and stop ModemManager from probing it. They
|
||
matter only for the [serial protocol](https://git.byonoy.com/public/abs96serial),
|
||
not for the SDK.
|
||
|
||
### macOS and Windows
|
||
|
||
Neither needs a driver or a permission step. On **macOS**, though, the device
|
||
can only be opened while a user is logged in to a desktop session on the Mac.
|
||
With nobody logged in — for example over SSH right after a restart — the device
|
||
is still listed by `available_devices()`, but opening it fails with
|
||
`DEVICE_COMMUNICATION_FAILURE` (`0x0006`). For an unattended Mac, enable
|
||
automatic login.
|
||
|
||
### When the device stops measuring
|
||
|
||
**This is not normal operation.** A healthy instrument measures every time, and
|
||
you should not need this section. It is here so that if you meet the state, you
|
||
recognise it rather than concluding your code is wrong.
|
||
|
||
A device can end up unable to measure while every diagnostic insists it is fine:
|
||
**every** measurement fails immediately with `DEVICE_OPERATION_FAILED`, in fresh
|
||
processes too, yet the device status reads OK, the handle reports open, and the
|
||
device error accessor reports nothing. With protocol logging on, the device
|
||
refuses each measurement request with return code 3.
|
||
|
||
To confirm the state quickly on a luminescence or fluorescence device, measure
|
||
with **no wells selected**: a healthy device returns `NO_ERROR` within a
|
||
fraction of a second; a device in this state returns `DEVICE_OPERATION_FAILED`
|
||
just as fast.
|
||
|
||
It has been seen after a measurement was stopped by ending the process —
|
||
including a long `ULTRA_SENSITIVE` read stopped before it finished. Waiting
|
||
does not clear it; a power cycle does.
|
||
|
||
Recovery is a device reboot. The internal variant can do this from software
|
||
(`reboot()`); **the public variant cannot** — unplug the device and plug it back
|
||
in. Either way it re-enumerates within a few seconds and you open it again.
|
||
|
||
> **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
|
||
- **[Native SDK →](SDK_NATIVE.md)**
|