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 — a wheel you
import. Scripting, tests, automation. - Native SDK — 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; 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 or Native 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 |
| Public native SDK | Self-serve, no credentials — the releases page |
| 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 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:
export BYONOY_USER='<your gitea username>'
export BYONOY_TOKEN='<your gitea token>'
$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.
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_supportedpredicates only. Helper predicates can answerTrueon a device that cannot perform the modality: an Absorbance One reportsabs96_available_wavelengths_supportedasTrue, and an Absorbance 96 Automate reportsabsone_available_wavelength_supportedasTrue, while the corresponding*_measurement_supportedisFalse. abs96_modules_supported,abs96_get_modulesandabs96_setup_modulesconcern the Absorbance 96's wavelength modules and are not needed for measuring. Do not callabs96_setup_modulesunless 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 asAbsorbanceOneOr96. 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); 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_SENSITIVEread 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). If 10 minutes per read is too long for you, useSENSITIVEor select fewer wells. CUSTOMis 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_measurenever returned — without an initialise it did not returnNOT_INITIALIZED— whether or not initialisation had been attempted. Treat it as a rule: callabsone_measureonly when the device status isOKandabsone_is_initializedreportsTrue(see 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 |
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_readoutis 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, 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.
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 |
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 — the same rules the Byonoy desktop
app installs. The file sits next to this guide; copy it onto the machine, then:
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:
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 thedialoutgroup — theusermodline 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,
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 → — including a complete measure-to-CSV program covering every device
- Native SDK →