Add SDK guide for the Byonoy device library
This commit is contained in:
@@ -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"
|
||||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user