2026-10-05 09:53:24 +00:00
2026-10-01 15:41:47 +02:00
2026-10-05 09:53:24 +00:00

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_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); 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). 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).
  • 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_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, 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 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, 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

S
Description
No description provided
Readme
158 KiB
v2026.09.1
Latest
2026-10-01 11:16:06 +02:00
Languages
Markdown 100%