Files

497 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)).
- **The library's own wait for an absorbance result is 110 s** by default, after
which the call returns an error. The device can extend that wait, so it is
not a guaranteed bound.
**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 (`byonoy_enable_logging` in C, `enable_logging` in
Python — it goes to the process's standard output), 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.
If the device is stuck again straight after a power cycle, check whether a
process was ended in the middle of a call in between — a watchdog killing a
slow first call is enough to put it back. Power-cycle it again, and give the
first calls time to complete.
> **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)**