Add SDK guide for the Byonoy device library

This commit is contained in:
2026-10-01 10:04:20 +02:00
commit 4864916a3a
4 changed files with 2321 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Byonoy device access rules.
#
# version: 1
SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="ab12", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="usb", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="106a", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="usb", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="1199", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="usb", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="119a", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="usb", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="119b", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="usb", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="12f1", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="usb", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="12f2", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="usb", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="12f3", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="ab12", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="106a", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="1199", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="119a", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="119b", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="12f1", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="12f2", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="12f3", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="usb", ATTRS{idVendor}=="0403", ATTRS{manufacturer}=="Byonoy GmbH", GROUP="dialout", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{manufacturer}=="Byonoy GmbH", GROUP="dialout", MODE="0660", TAG+="uaccess", ENV{ID_MM_DEVICE_IGNORE}="1"
+484
View File
@@ -0,0 +1,484 @@
# 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.
> **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='<your gitea username>'
export BYONOY_TOKEN='<your gitea token>'
```
```powershell
$env:BYONOY_USER='<your gitea username>'
$env:BYONOY_TOKEN='<your gitea token>'
```
Commands in these documents are written for **bash** (Linux, macOS). Where
Windows differs, the Python document gives the PowerShell equivalent.
---
## 3. How the library works
These apply to both forms and both variants. The per-form documents show the
code; this is the behaviour behind it.
**A device must be physically attached over USB.** There is no simulator. An
empty device list is the normal answer when nothing is plugged in, not an error.
For prototyping in Python, a small mock can stand in for a device — see
[Prototyping without a device](SDK_PYTHON.md#7-prototyping-without-a-device).
**The lifecycle is discover → open → use → close.** Opening gives you a handle;
everything else takes that handle; closing invalidates it. Using a closed handle
returns `INVALID_ARGUMENT`, not `DEVICE_CLOSED`.
**Check every return code.** Almost every call reports one. A value handed back
alongside a non-`NO_ERROR` code is meaningless — and it is not empty: a failed
96-well measurement still hands back 96 zeros, so testing the result's length
or truthiness does not tell you it worked.
**Ask what the device supports before asking it to do something.** Each
modality and feature has a `*_supported` predicate; calling something the device
does not support gives `UNSUPPORTED_OPERATION`. Do not branch on the model name.
- **Decide the modality from the `*_measurement_supported` predicates only.**
Helper predicates can answer `True` on a device that cannot perform the
modality: an Absorbance One reports `abs96_available_wavelengths_supported`
as `True`, and an Absorbance 96 Automate reports
`absone_available_wavelength_supported` as `True`, while the corresponding
`*_measurement_supported` is `False`.
- `abs96_modules_supported`, `abs96_get_modules` and `abs96_setup_modules` concern
the Absorbance 96's wavelength modules and are not needed for measuring. Do
not call `abs96_setup_modules` unless Byonoy asks you to.
- **Discovery and device information can name different types.** Discovery
(`available_devices()`) reports the type from the USB IDs alone, and every
Absorbance One is reported there as `AbsorbanceOneOr96`. Once the device is
open, `get_device_information()` reports what it actually is
(`AbsorbanceOne`). Treat the latter as authoritative.
**One open handle per device.** What happens when you try a second one
depends on who holds the first:
- another handle in **your own process** → `DEVICE_ALREADY_OPEN` (`0x0104`)
- the device held by **another process**, on Linux and macOS →
`DEVICE_COMMUNICATION_FAILURE` (`0x0006`)
- the device held by **another process, on Windows** → the second open
**succeeds**, and both processes can talk to the device at once. Nothing
stops them interleaving commands. On Windows, making sure only one program
uses a device is your job.
`0x0006` does not name its cause. Before treating it as a hardware fault, check
the usual ones: another program — a stray REPL, a previous run, another tool —
still has the device open; on Linux, the device is not accessible to your user
(see [Linux permissions](#linux-permissions)); on macOS, nobody is logged in at
the machine (see below).
**Measurements block, and can take a long time.** Ctrl-C will not interrupt a
measurement in progress — SIGINT is ignored while the call is inside the
library. Size timeouts from the mode you actually use.
The luminescence and fluorescence modes set an integration time:
`RAPID` 100 ms, `SENSITIVE` 2 s, `ULTRA_SENSITIVE` 20 s, or
`custom_integration_time_ms` for `CUSTOM` (at least 50). How that turns into a
duration differs between the two devices.
**Luminescence 96** reads the plate through 16 detector channels of 6 wells
each, one channel after another, and spends the integration time — but at
least 300 ms — on each channel in use. With the device's sampling overhead a
run takes roughly *(channels used) × max(integration time, 300 ms) × 1.9*, so a
full plate is about 30 × the integration time, and `RAPID` is limited by the
300 ms floor. A channel counts as used when any of its 6 wells is selected;
which wells share a channel is fixed by the hardware, so selecting fewer wells
saves time only when it frees whole channels:
| Mode | Integration time | Full plate |
|---|---|---|
| `RAPID` | 100 ms | ~10 s |
| `SENSITIVE` | 2 s | ~60 s |
| `ULTRA_SENSITIVE` | 20 s | **~10 min** |
| `CUSTOM` | `custom_integration_time_ms`, at least 50 | ~30 × that, plus overhead |
**Fluorescence 96** steps across the plate one column at a time and measures
every column that contains **at least one selected well**; columns with no
selected well are skipped entirely. A run therefore takes roughly
*(number of columns used) × (time per column)*, plus a short start-up, and the
time per column grows with the integration time. Deselecting wells within a
column that is measured anyway saves nothing — to shorten a run, leave out
whole columns. The exact time per column depends on the device's calibration,
so it differs between units.
**Absorbance:**
| Device | Call | Duration |
|---|---|---|
| Absorbance 96 Automate | initialise, one wavelength | 1.3–2 s (~2.7 s with a reference wavelength) |
| Absorbance 96 Automate | initialise, two wavelengths | ~3 s |
| Absorbance 96 Automate | measure, one wavelength | ~1.2 s (~0.7 s in rapid mode) |
| Absorbance 96 Automate | measure, two wavelengths | ~2.8 s |
| Absorbance One | measure | under 5 s |
- **Do not stop a long measurement early.** An `ULTRA_SENSITIVE` read of a full
plate takes about 10 minutes. On a test unit, one that was ended after 9
minutes left the device unable to measure until it was power-cycled (see
[When the device stops measuring](#when-the-device-stops-measuring)). If
10 minutes per read is too long for you, use `SENSITIVE` or select fewer
wells.
- **`CUSTOM` is rejected below 50 ms** (`INVALID_ARGUMENT`). There is no upper
limit — the device splits a long integration into shorter steps — and the
duration follows the formulas above.
- **A measurement can block indefinitely instead of failing.** On an Absorbance
One in an error state, `absone_measure` never returned — without an
initialise it did **not** return `NOT_INITIALIZED` — whether or not
initialisation had been attempted. Treat it as a rule: call `absone_measure`
only when the device status is `OK` and `absone_is_initialized` reports
`True` (see [Device status and device error](#device-status-and-device-error)).
**Stopping a blocked measurement is a last resort.** The only way out of a call
that does not return is to end the process; the library's own internal waits
do not make it return. Ending the process works — SIGTERM or SIGKILL on Linux
and macOS, `Stop-Process` on Windows. The library gets no chance to
clean up, and the device may be left unable to measure, or may reset itself
— dropping off USB and re-enumerating a few seconds to a minute later. Plan for
a replug afterwards. In a virtual machine with USB passthrough, a device that
re-enumerates comes back to the **host**, not the VM, and has to be attached
to the VM again. If you run unattended, run each
measurement in a child process under a timeout (the Python document shows a
portable way) so that a hang costs you the measurement, not the whole program.
**The library does not check for a plate,** and no device refuses to measure
without one. Absorbance measurements on an empty slot return `NO_ERROR` and
near-zero values that mean nothing; a Luminescence 96 cannot even tell whether
a plate is inserted, and without one returns background noise around zero.
Where the device can sense the slot, checking it before measuring is your job;
the complete Python example shows how.
**A measurement returns a fixed-size result.** A 96-well read gives 96 values
whatever you selected; index `i` of the result corresponds to index `i` of your
selection. Deselecting wells does not shorten the list.
### Well order
**Results are in row-major order.** On a 96-well plate (8 rows × 12 columns),
index `i` is row `i // 12` and column `i % 12`:
| Index | 0 | 1 | … | 11 | 12 | … | 95 |
|---|---|---|---|---|---|---|---|
| Well | A1 | A2 | … | A12 | B1 | … | H12 |
```python
def well_name(i, columns=12):
return f"{chr(ord('A') + i // columns)}{i % columns + 1}"
```
The same order applies to `selected_wells`: entry `i` selects well `i` in the
table above. It holds for every 96-well modality (absorbance, luminescence,
fluorescence). The device sends one plate row at a time and nothing between it
and your code reorders the values, so the order is fixed by the plate, not by
the readout.
- **Bottom readout needs no correction.** A device that reads the plate from
below mirrors the data itself before sending it. The result is already in
plate orientation; do not flip it again because `is_bottom_readout` is set.
### Deselected wells
A deselected well keeps its place in the result, but what it contains differs
by modality — do not treat it as a reading:
| Modality | Deselected well contains |
|---|---|
| Luminescence 96 | `0` |
| Fluorescence 96 | `NaN` |
| Absorbance 96 Automate | no well selection; every well is measured |
Because Luminescence 96 reports deselected wells as `0`, you cannot tell one
from a genuine dark reading by value. Keep your own selection and use it to
filter the result.
### Units
| Modality | Unit | Meaning |
|---|---|---|
| Absorbance (Absorbance 96 Automate, Absorbance One) | OD — optical density | how much light the sample absorbs, on a log₁₀ scale: 0 lets all light through, 1 lets 10 % through, 2 lets 1 % through |
| Luminescence | RLU — relative light units | how much light the sample emits, on the instrument's own scale |
| Fluorescence | RFU — relative fluorescence units | how much light the sample emits when excited, on the instrument's own scale |
RLU and RFU are not calibrated physical units: compare readings taken on the
same device, in the same mode (and for fluorescence, the same filter set),
rather than across devices or settings.
### Device status and device error
Two calls describe the device's own health, separately from the return code of
any single call:
**`get_device_status`** asks the device and gives a state:
| State | Meaning |
|---|---|
| `OK` | the device reports no error |
| `ERROR` | the device reports an error — see `get_device_error` |
| `BROKEN_FW` | the firmware is corrupted or unidentifiable; the device needs a firmware update |
| `UNKNOWN` | the device did not answer the status request |
Anything but `OK` means: do not measure.
**`get_device_error`** gives the last error recorded for the device, from a
small set shared by all devices. It does not ask the device itself — call
`get_device_status` first for a current value.
| Device error | Meaning |
|---|---|
| `0` | no error |
| `0x8001` (32769) | the device reported an error of its own — the common case; see below |
| `0x8004` (32772) | the device did not respond |
| `0x8009` (32777) | the device was disconnected |
| `0x8000` (32768) | unknown error |
These numbers are **not** the library's return codes listed under
[Error codes worth recognising](#error-codes-worth-recognising), even where
they coincide (`0x8001` there is `MEASUREMENT_SLOT_NOT_EMPTY`).
**What the device actually reported** is not available through the API. It
appears only in the protocol log (`enable_logging`), as a text ID such as
`com.byonoy-AbsOne-MIN_LIGHT_ERROR`. For the two absorbance devices, these mean:
| Text ID | Meaning |
|---|---|
| `AbsOne-AMBIENT_LIGHT_ERROR` | too much ambient light is reaching the device, or it is defective |
| `AbsOne-MIN_LIGHT_ERROR` | too little light: the device is dirty, the slot is occupied, or it is defective |
| `AbsOne-TIMEOUT_ERROR` | the measurement was disrupted, e.g. by a shadow in the device |
| `AbsOne-NOISE_LIMIT_ERROR` | USB cable or hub problem, or a defective device |
| `AbsOne-HARDWARE_ERROR`, `AbsOne-UNRECOVERABLE_ERROR` | the device is defective; nothing the user can do |
| Absorbance 96: calibration failed | initialise failed — usually a plate in the slot or a dirty slot |
| Absorbance 96: ambient light | too much light is entering the device |
| Absorbance 96: USB power | insufficient USB power — use another port or a powered hub |
| Absorbance 96: temperature | the device temperature is outside its specified range |
| Absorbance 96: measurement unit | no contact with the upper section — make sure the device parts are aligned and seated |
| Absorbance 96: hardware | hardware defect — contact support |
For the luminescence and fluorescence devices the log shows only the raw
firmware code, as `errorCode=0x…`. On a **Luminescence 96** it is a single
value:
| Code | Meaning | Cleared by |
|---|---|---|
| `0x1` | calibration missing — the detector calibration could not be loaded | a power cycle; report it |
| `0x7` | shutter error — the shutter failed to move after repeated retries during a measurement | a power cycle |
On a **Fluorescence 96** it is a set of flags, several of which can be set at
once:
| Bit | Value | Meaning |
|---|---|---|
| 0 | `0x01` | calibration missing |
| 1 | `0x02` | configuration — the device's identity or board revision could not be read |
| 2 | `0x04` | homing — the slide is not homed; check that it moves freely and the transport lock is out |
| 3 | `0x08` | detector — usually the detector sled's supply or connection rather than the photodiodes |
| 4 | `0x10` | hall position — the position sensor does not answer |
| 5 | `0x20` | accelerometer — the accelerometer does not answer |
| 6 | `0x40` | photodiode compensation — no excitation light seen |
| 7 | `0x80` | motion system — the slide stalled, or homing never found its endstop |
A Fluorescence 96 clears all flags at the start of every measurement and on a
restart, so a code seen after a failed run describes that run. Any flag
aborts a running measurement. Report codes to Byonoy support with the serial
number and firmware version.
`ERROR` clears by itself once the device stops reporting the condition; no
reconnect is needed. A device that keeps reporting one — such as an Absorbance
One with a dirty optical path — stays in `ERROR` across closing, reopening and
new processes. It still answers queries, but its measurements fail or hang.
The opposite case — status `OK`, error `0`, yet every measurement fails — is
described under [When the device stops measuring](#when-the-device-stops-measuring).
### Error codes worth recognising
The library defines 25 error codes; these are the ones you are most likely to
meet. Print the name and number (`rc.name`, `hex(rc.value)` in Python) rather
than assuming an unlisted value is impossible.
| Code | Name | Usual cause |
|---|---|---|
| `0x0000` | `NO_ERROR` | success |
| `0x0002` | `DEVICE_CLOSED` | device unplugged or lost while open |
| `0x0003` | `INVALID_ARGUMENT` | a handle you already freed, a config you did not create, or a value the device does not accept (see also the Absorbance 96 initialise note in the Python document) |
| `0x0005` | `UNSUPPORTED_OPERATION` | wrong modality or feature for this device |
| `0x0006` | `DEVICE_COMMUNICATION_FAILURE` | another process holds the device; on Linux, missing permissions; on macOS, nobody logged in to a desktop session; otherwise cable, power, or a device mid-reset |
| `0x0007` | `DEVICE_OPERATION_FAILED` | the device refused the operation. If the device status is `ERROR`, that is the cause; if it is `OK` and every measurement fails this way, see [When the device stops measuring](#when-the-device-stops-measuring) |
| `0x0101` | `DEVICE_NOT_FOUND` | nothing matching attached |
| `0x0104` | `DEVICE_ALREADY_OPEN` | your own process already has it open |
| `0x8001` | `MEASUREMENT_SLOT_NOT_EMPTY` | absorbance: initialising with a plate or cuvette in the slot |
| `0x8002` | `NOT_INITIALIZED` | absorbance: measuring a wavelength that was not initialised **on this handle** — initialisation does not survive closing the device |
| `0x8003` | `MEASUREMENT_PARTS_NOT_ALIGNED` | the device's parts are not seated correctly |
### Linux permissions
USB access is root-only by default. Without a udev rule the device still
appears in `available_devices()`, but with an **empty serial number**, and
opening it fails with `DEVICE_COMMUNICATION_FAILURE` (`0x0006`). Rather than
running as root, install the suggested rules in
**[`70-byonoy.rules`](70-byonoy.rules)** — the same rules the Byonoy desktop
app installs. The file sits next to this guide; copy it onto the machine, then:
```bash
sudo install -m 0644 70-byonoy.rules /etc/udev/rules.d/70-byonoy.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --subsystem-match=usb --subsystem-match=hidraw --subsystem-match=tty
sudo usermod -aG dialout "$USER" # needed for SSH, services and CI; then log in again
```
Check that it took effect, in a new login:
```bash
id | grep -o dialout # your account is in the group
lsusb -d 16d0: # e.g. "Bus 003 Device 006: ID 16d0:106a …"
ls -l /dev/bus/usb/003/006 # the Bus/Device numbers from lsusb: group dialout
```
The library talks to the device through libusb, so the USB node under
`/dev/bus/usb` is the one whose permissions matter. The device's
`/dev/hidraw*` node is not a reliable check: it disappears while a program has
the device open, and can stay missing after a program was killed even though
the device works. If the USB node's group is not `dialout`, unplug the device
and plug it back in.
An empty serial number is not always a permissions problem. After a process
was ended in the middle of a measurement, a device can be listed with an empty
serial number while opening it returns `DEVICE_COMMUNICATION_FAILURE`
(`0x0006`) or `DEVICE_NOT_FOUND` (`0x0101`) — typically because it is resetting
and about to drop off USB and re-enumerate. If permissions were fine before,
wait a minute and look again; if it does not come back, replug it.
The rules cover Byonoy devices by USB vendor and product ID (vendor `16d0`,
plus `0483:ab12`), for both the raw USB and the `hidraw` device nodes. They
grant access in two ways:
- `TAG+="uaccess"` gives the user logged in at the machine's own seat access
automatically. This does **not** apply to SSH sessions, containers, CI
runners or services.
- `GROUP="dialout"` gives access to members of the `dialout` group — the
`usermod` line above. Anything that is not a local desktop session depends on
it.
The last two lines match the manual Absorbance 96's serial adapter (vendor
`0403`, manufacturer `Byonoy GmbH`) and stop ModemManager from probing it. They
matter only for the [serial protocol](https://git.byonoy.com/public/abs96serial),
not for the SDK.
### macOS and Windows
Neither needs a driver or a permission step. On **macOS**, though, the device
can only be opened while a user is logged in to a desktop session on the Mac.
With nobody logged in — for example over SSH right after a restart — the device
is still listed by `available_devices()`, but opening it fails with
`DEVICE_COMMUNICATION_FAILURE` (`0x0006`). For an unattended Mac, enable
automatic login.
### When the device stops measuring
**This is not normal operation.** A healthy instrument measures every time, and
you should not need this section. It is here so that if you meet the state, you
recognise it rather than concluding your code is wrong.
A device can end up unable to measure while every diagnostic insists it is fine:
**every** measurement fails immediately with `DEVICE_OPERATION_FAILED`, in fresh
processes too, yet the device status reads OK, the handle reports open, and the
device error accessor reports nothing. With protocol logging on, the device
refuses each measurement request with return code 3.
To confirm the state quickly on a luminescence or fluorescence device, measure
with **no wells selected**: a healthy device returns `NO_ERROR` within a
fraction of a second; a device in this state returns `DEVICE_OPERATION_FAILED`
just as fast.
It has been seen after a measurement was stopped by ending the process —
including a long `ULTRA_SENSITIVE` read stopped before it finished. Waiting
does not clear it; a power cycle does.
Recovery is a device reboot. The internal variant can do this from software
(`reboot()`); **the public variant cannot** — unplug the device and plug it back
in. Either way it re-enumerates within a few seconds and you open it again.
> **Report it if it recurs.** A one-off on an engineering or pre-production unit
> is not remarkable. A device that needs rebooting repeatedly, or one on which a
> particular mode fails consistently, is a fault worth reporting to Byonoy
> support with the serial number, the firmware version from the device
> information call, the mode and well selection you used, and how often it
> happens. Do not work around it with a reboot loop — that hides a hardware
> problem behind software.
---
## 4. Next
- **[Python SDK →](SDK_PYTHON.md)** — including a [complete measure-to-CSV program](SDK_PYTHON.md#6-complete-example-measure-and-export-csv) covering every device
- **[Native SDK →](SDK_NATIVE.md)**
+311
View File
@@ -0,0 +1,311 @@
# Byonoy Device Library — Native SDK
Read the **[SDK guide](README.md)** first: it explains the two
variants, how the library behaves, and the error codes. This document is the
C/C++ specifics.
Everything up to section 6 is the **public** variant. Section 6 covers what the
internal variant adds.
---
## 1. Getting the SDK
**The native SDK is available on request, in either variant.** It is not
published anywhere you can fetch it from without Byonoy providing it. Ask for
the variant you need and you will be given an archive,
`byonoy-devices-public-sdk-<version>.zip` or
`byonoy-devices-internal-sdk-<version>.zip`.
If you have been given access to the internal repository's releases, the archive
is attached to each release and can be downloaded with your token:
```bash
TAG=$(curl -fsS -H "Authorization: token $BYONOY_TOKEN" \
'https://git.byonoy.com/api/v1/repos/sw/byonoy_device_library/releases?limit=1' \
| python3 -c 'import json,sys; print(json.load(sys.stdin)[0]["tag_name"])')
curl -fsSL -H "Authorization: token $BYONOY_TOKEN" -o sdk.zip \
"https://git.byonoy.com/sw/byonoy_device_library/releases/download/${TAG}/byonoy-devices-public-sdk-${TAG}.zip"
unzip -q sdk.zip -d sdk
```
Use that `releases/download/...` URL; the `/api/v1/.../releases/assets/<id>`
endpoint returns `404` on this instance.
`limit=1` takes the newest release, which may be a pre-release (a tag with a
`-suffix`). Public archives are attached to final tags; a recent pre-release tag
carries internal artifacts only. Check what a release actually has rather than
assuming.
---
## 2. Layout
One archive carries every platform the release was built for; take the files for
yours, and check yours is present before building.
```
sdk/include/byonoy_device_library.h the API
sdk/lib/libbyonoy_device_library.so Linux
sdk/lib/libbyonoy_device_library.dylib macOS
sdk/bin/libbyonoy_device_library.dll Windows runtime
sdk/lib/libbyonoy_device_library.dll.a Windows import library
sdk/lib/libhidapi* dependency, ships alongside
sdk/examples/C, sdk/examples/C++ compilable references
sdk/third-party-licenses/ notices for bundled dependencies
```
The internal archive is the same with `_internal` appended to every library
name, plus a second header. `bin/` holds the Windows runtime only — on macOS and
Linux everything you need is in `lib/`.
---
## 3. Build and run
Compiling is not enough: **you must also tell your binary where to find the
library at run time.** The shipped library's install name is
`@rpath/libbyonoy_device_library.dylib`, and its own RPATH entries only cover how
*it* finds hidapi — they do nothing for your executable. Link without an RPATH of
your own and you get a clean compile followed by:
```
dyld: Library not loaded: @rpath/libbyonoy_device_library.dylib
Reason: no LC_RPATH's found
```
Give the executable an RPATH, relative to itself so the result stays portable:
```bash
# macOS
cc -I sdk/include main.c -L sdk/lib -lbyonoy_device_library \
-Wl,-rpath,@executable_path/sdk/lib -o demo
# Linux
cc -I sdk/include main.c -L sdk/lib -lbyonoy_device_library \
-Wl,-rpath,'$ORIGIN/sdk/lib' -o demo
```
Adjust the RPATH to wherever `lib/` sits relative to the finished binary. As a
throwaway alternative you can set the loader path in the environment, but this
does not travel with the binary:
```bash
DYLD_LIBRARY_PATH=sdk/lib ./demo # macOS
LD_LIBRARY_PATH=sdk/lib ./demo # Linux
```
On Windows, put `sdk/bin` on `PATH`; the DLLs there must travel with the
executable.
The examples below use `uint32_t` and `true`, which the SDK header provides on a
current compiler. Add `#include <stdint.h>` and `<stdbool.h>` if yours is older
than C23.
---
## 4. Smallest complete program
```c
#include "byonoy_device_library.h"
#include <stdio.h>
int main(void) {
byonoy_device_t* devices = NULL;
uint32_t count = 0;
byonoy_available_devices(&devices, &count); /* returns void */
if (count == 0) {
printf("no device\n");
byonoy_free_available_devices(); /* allocated even when empty */
return 1;
}
byonoy_device_handle_t handle;
byonoy_error_code rc = byonoy_open_device(devices, &handle);
byonoy_free_available_devices(); /* list is dead once opened */
if (rc != BYONOY_ERROR_NO_ERROR) {
printf("open failed: 0x%04x\n", rc);
return 1;
}
byonoy_device_info_t* info = NULL;
rc = byonoy_create_device_information(&info);
if (rc != BYONOY_ERROR_NO_ERROR) { /* create/free pair */
printf("allocation failed: 0x%04x\n", rc);
byonoy_free_device(handle);
return 1;
}
rc = byonoy_get_device_information(handle, info);
if (rc == BYONOY_ERROR_NO_ERROR) {
printf("%s %s %s\n", info->ref_no, info->sn, info->version);
} else {
printf("device info failed: 0x%04x\n", rc);
}
byonoy_free_device_information(info);
byonoy_free_device(handle);
return rc == BYONOY_ERROR_NO_ERROR ? 0 : 1;
}
```
`byonoy_device_info_t` carries `sn`, `ref_no` and `version` as `const char*`,
and `type` as a `byonoy_device_types` enum.
### Memory rules
- Anything returned through a pointer has a **`create`/`free` pair**. Call
`create` first, `free` exactly once, and never free something you did not
create.
- Functions returning `int`, `float` or `bool` through a pointer allocate
nothing.
- `byonoy_available_devices()` allocates a list; release it with
`byonoy_free_available_devices()` once you have opened what you need. The
`byonoy_device_t*` entries are invalid afterwards — the **handle** is what
stays valid.
- `byonoy_free_device(handle)` closes the device. The handle is invalid after
that, and calls with it return `BYONOY_ERROR_INVALID_ARGUMENT` (`0x0003`) —
*not* `DEVICE_CLOSED`, which is what a disconnected but still-open device
gives you.
---
## 5. Taking a measurement
Gate on the predicate, `create` the config and result, measure, free both. A
complete program, so it can be compiled as it stands:
```c
#include "byonoy_device_library.h"
#include <stdio.h>
int main(void) {
byonoy_device_t* devices = NULL;
uint32_t count = 0;
byonoy_available_devices(&devices, &count);
if (count == 0) { printf("no device\n"); byonoy_free_available_devices(); return 1; }
byonoy_device_handle_t handle;
byonoy_error_code rc = byonoy_open_device(devices, &handle);
byonoy_free_available_devices();
if (rc != BYONOY_ERROR_NO_ERROR) { printf("open failed: 0x%04x\n", rc); return 1; }
if (!byonoy_lum96_measurement_supported(handle)) {
printf("this device does not do 96-well luminescence\n");
byonoy_free_device(handle);
return 1;
}
byonoy_lum96_measurement_config_t* config = NULL;
byonoy_lum96_measurement_result_t* result = NULL;
if (byonoy_create_lum96_measurement_config(&config) != BYONOY_ERROR_NO_ERROR ||
byonoy_create_lum96_measurement_result(&result) != BYONOY_ERROR_NO_ERROR) {
printf("allocation failed\n");
byonoy_free_device(handle);
return 1;
}
config->mode = BYONOY_LUM96_INTEGRATION_RAPID; /* ~10 s; SENSITIVE is ~60 s */
for (int i = 0; i < 96; ++i) config->selected_wells[i] = true;
rc = byonoy_lum96_measure(handle, config, result);
if (rc == BYONOY_ERROR_NO_ERROR) {
for (int i = 0; i < 96; ++i) {
printf("%8.1f", result->value[i]);
if ((i + 1) % 12 == 0) printf("\n");
}
} else {
printf("measurement failed: 0x%04x\n", rc);
}
byonoy_free_lum96_measurement_result(result);
byonoy_free_lum96_measurement_config(config);
byonoy_free_device(handle);
return rc == BYONOY_ERROR_NO_ERROR ? 0 : 1;
}
```
**Set both `mode` and `selected_wells` explicitly.** A freshly created config
has every well deselected, and measuring with it *succeeds* — returning
instantly with 96 zeros and no error. Its default mode is
`BYONOY_LUM96_INTEGRATION_RAPID` (0), which differs from the Python binding's
default; do not rely on either.
The mode enum is
`BYONOY_LUM96_INTEGRATION_{RAPID,SENSITIVE,ULTRA_SENSITIVE,CUSTOM}`, and
`result->value` is a fixed `float[96]` in row-major
[well order](README.md#well-order) (`value[0]` is A1, `value[12]` is B1), as
is `selected_wells`. The capability table in the
[Python document](SDK_PYTHON.md#which-modality-does-this-device-support) is also
the C capability table, with `byonoy_` prefixes.
### Bundled examples
`sdk/examples/` holds compilable references, one directory each with a `main.c`
or `main.cpp` and a `CMakeLists.txt`:
| Path | Shows |
|---|---|
| `examples/C/device-info` | open, read information, close |
| `examples/C/lum96-measurement` | 96-well luminescence |
| `examples/C/abs96-multi-measurement` | multi-wavelength absorbance |
| `examples/C/absone-measurement` | single-cuvette absorbance |
| `examples/C/device-update` | firmware update |
| `examples/C++/async` | asynchronous measurement (internal variant) |
| `examples/C++/rpc` | device RPC (internal variant) |
They build with CMake, and CMake sets the RPATH for you — which makes this the
safer route than the `cc` line above:
```bash
cmake -S sdk/examples/C/device-info -B build-example
cmake --build build-example
./build-example/device-information
```
Against the **internal** archive their `find_library` call fails, because they
look for the public library name while the internal archive ships
`libbyonoy_device_library_internal.*`. Point the cache variable at the real file:
```bash
cmake -S sdk/examples/C/device-info -B build-example \
-DBYONOY_DEVICE_LIBRARY="$PWD/sdk/lib/libbyonoy_device_library_internal.dylib"
```
---
## 6. The internal variant
Available on request; see [the SDK guide](README.md#1-which-variant-public-or-internal)
for when you should want it.
**The C API is identical.** Everything above applies unchanged except the
library name — link `-lbyonoy_device_library_internal` instead. The extra
functionality arrives as a *second* header:
```
sdk/include/byonoy_device_library.h same as public
sdk/include/byonoy_device_library_internal.h the additions
```
`byonoy_device_library_internal.h` is **C++, not C**. It declares
`namespace byonoy::device::library::internal` and uses `std::vector`,
`std::optional`, `std::filesystem` and `std::chrono` in its signatures, so a
translation unit including it must be compiled as C++. The public header remains
usable from C either way — the internal header wraps its include in
`extern "C"`.
It adds its own error enum, `byonoy_internal_error_code`, based at `0x10000`
(`NOT_ENUMERATED`, `UNKNOWN_NAME`, `UNKNOWN_ID`, `REQUEST_FAILED`, `WRONG_TYPE`,
`READ_ONLY`, `WRITE_ONLY`, `FILE_READ_FAILED`, `FILE_WRITE_FAILED`,
`ASYNC_OPERATION_ALREADY_RUNNING`, …). These are distinct from the public codes
in the SDK guide's table, not additions to them.
The feature areas — asynchronous measurements, data fields, files, RPC,
diagnostics, LEDs, bootloader and flashing, reboot — are listed with their entry
points in the [Python document](SDK_PYTHON.md#what-it-adds); the C++ names match
apart from the namespace. Each is gated on its own `*_supported` predicate.
`sdk/examples/C++/async` and `sdk/examples/C++/rpc` are worked examples of two
of these, and are the best starting point for the C++ header.
+1502
View File
File diff suppressed because it is too large Load Diff