# 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 | | **Internal Python wheel** | On request from Byonoy; installed from the internal registry with a token | | **Native SDK, either variant** | On request from Byonoy | Only the public Python wheel is self-serve today. The native archive is not currently published to a location you can reach without Byonoy handing it to you, whichever variant you need. 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='' export BYONOY_TOKEN='' ``` ```powershell $env:BYONOY_USER='' $env:BYONOY_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)**