Files

524 lines
20 KiB
Markdown

# 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 public native SDK is a free download.** Each release on the
[releases page](https://git.byonoy.com/public/byonoy_devices_sdk/releases) carries it as `byonoy-devices-public-sdk-<version>.zip`,
next to a `SHA256SUMS` file. No account or token is needed:
```bash
TAG=v2026.09.1
curl -fsSLO "https://git.byonoy.com/public/byonoy_devices_sdk/releases/download/${TAG}/byonoy-devices-public-sdk-${TAG}.zip"
curl -fsSLO "https://git.byonoy.com/public/byonoy_devices_sdk/releases/download/${TAG}/SHA256SUMS"
sha256sum --ignore-missing -c SHA256SUMS # macOS: shasum -a 256 --ignore-missing -c SHA256SUMS
unzip -q "byonoy-devices-public-sdk-${TAG}.zip" -d sdk
```
In Windows PowerShell — `curl.exe`, not `curl`, which is an alias for
`Invoke-WebRequest` there:
```powershell
$TAG = (Invoke-RestMethod https://git.byonoy.com/api/v1/repos/public/byonoy_devices_sdk/releases/latest).tag_name
$base = "https://git.byonoy.com/public/byonoy_devices_sdk/releases/download/$TAG"
$zip = "byonoy-devices-public-sdk-$TAG.zip"
curl.exe -fsSLO "$base/$zip"
curl.exe -fsSLO "$base/SHA256SUMS"
$expected = ((Select-String -Path SHA256SUMS -SimpleMatch $zip).Line -split '\s+')[0]
if ((Get-FileHash $zip -Algorithm SHA256).Hash -ne $expected.ToUpper()) { throw "checksum mismatch" }
Expand-Archive $zip -DestinationPath sdk
```
**The internal native SDK is available on request**, as
`byonoy-devices-internal-sdk-<version>.zip`. If you have been given access to
the internal repository's releases, it is attached to each release there 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-internal-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`). 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/bin/libhidapi.dll, libgcc_s_seh-1.dll,
libstdc++-6.dll, libwinpthread-1.dll Windows dependencies, ship alongside
sdk/lib/libbyonoy_device_library.dll.a Windows import library (GNU format)
sdk/lib/libhidapi* Linux/macOS dependency, ships alongside
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 and C++ examples. `bin/` holds the Windows runtime only — on macOS and
Linux everything you need is in `lib/`.
---
## 3. Build and run
### Prerequisites
A C compiler, CMake for the bundled examples, and `unzip`:
| Platform | Install |
|---|---|
| Debian, Ubuntu | `sudo apt update && sudo apt install -y build-essential cmake unzip` |
| Fedora | `sudo dnf install gcc make cmake unzip` |
| macOS | `xcode-select --install`, then `brew install cmake` |
| Windows | a MinGW-w64 toolchain — see below |
On **Windows**, the library is a **MinGW-w64 build** (GCC, x86_64, SEH
exceptions, POSIX threads, msvcrt), and its import library
`libbyonoy_device_library.dll.a` is in GNU format. Use a matching MinGW-w64 GCC;
WinLibs provides one, with Ninja included:
```powershell
winget install BrechtSanders.WinLibs.POSIX.MSVCRT
winget install Kitware.CMake
```
Open a new shell afterwards so both are on `PATH`. Visual Studio (MSVC) cannot
use the shipped import library as it is.
### Finding the library at run time
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
Referenced from: … demo
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** there is no RPATH. Compile, then **copy all five DLLs from
`sdk\bin` next to the executable** — they travel with it:
```powershell
gcc -I sdk/include main.c -L sdk/lib -lbyonoy_device_library -o demo.exe
Copy-Item sdk\bin\*.dll .
.\demo.exe
```
A missing DLL produces no message: the program just exits with code
`0xC0000135` (`-1073741515`). Windows also looks in the current directory, so a
test run from inside the SDK folder can hide a DLL you forgot to copy. Do not
rely on adding `sdk\bin` to `PATH`: when a MinGW toolchain is on `PATH` too,
Windows may load the toolchain's own `libstdc++-6.dll` and `libgcc_s_seh-1.dll`
instead of the ones shipped with the SDK.
---
## 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"); /* nothing to free */
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()` hands out a list owned by the library; release
it with `byonoy_free_available_devices()` once you have opened what you need
(an empty list needs no release, though releasing it is harmless). 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 luminescence 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"); 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.
### Absorbance
An Absorbance 96 Automate needs more steps: check the device's health, query the
wavelengths (required before a single-wavelength initialise), initialise with
the slot **empty**, wait for the plate, then measure. The library checks
neither the slot nor the plate, so the program does. Like everything else
here, the status, slot and wavelength objects come in `create`/`free` pairs:
```c
#define _POSIX_C_SOURCE 200809L /* nanosleep */
#include "byonoy_device_library.h"
#include <stdio.h>
#if defined(_WIN32)
#include <windows.h>
static void sleep_ms(unsigned ms) { Sleep(ms); }
#else
#include <time.h>
static void sleep_ms(unsigned ms) {
struct timespec t = {ms / 1000, (long)(ms % 1000) * 1000000L};
nanosleep(&t, NULL);
}
#endif
/* Wait until the slot reads `wanted`, for at most `seconds`. */
static int wait_for_slot(byonoy_device_handle_t handle, byonoy_device_slot_status_t wanted, int seconds) {
byonoy_device_slot_status_t* slot = NULL;
if (byonoy_create_device_slot_status(&slot) != BYONOY_ERROR_NO_ERROR) return 0;
int reached = 0;
for (int i = 0; i < seconds * 2 && !reached; ++i) {
if (byonoy_get_device_slot_status(handle, slot) == BYONOY_ERROR_NO_ERROR && *slot == wanted) reached = 1;
else sleep_ms(500);
}
byonoy_free_device_slot_status(slot);
return reached;
}
int main(void) {
byonoy_device_t* devices = NULL;
uint32_t count = 0;
byonoy_available_devices(&devices, &count);
if (count == 0) { printf("no device\n"); 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; }
byonoy_abs96_wavelengths_t* wavelengths = NULL;
byonoy_abs96_single_measurement_config_t* config = NULL;
byonoy_abs96_single_measurement_result_t* result = NULL;
byonoy_device_status_t* status = NULL;
uint32_t device_error = 0;
rc = BYONOY_ERROR_UNKNOWN_ERROR;
if (!byonoy_abs96_measurement_supported(handle)) {
printf("this device does not do 96-well absorbance\n");
goto done;
}
/* Measure only a healthy device. */
if (byonoy_create_device_status(&status) != BYONOY_ERROR_NO_ERROR ||
byonoy_get_device_status(handle, status) != BYONOY_ERROR_NO_ERROR ||
byonoy_get_device_error(handle, &device_error) != BYONOY_ERROR_NO_ERROR) {
printf("status query failed\n");
goto done;
}
if (*status != BYONOY_DEVICE_STATE_OK || device_error != 0) {
printf("device status %d, device error 0x%x: not measuring\n", (int)*status, device_error);
goto done;
}
/* Required before a single-wavelength initialise; also says which are valid. */
if (byonoy_create_abs96_wavelengths(&wavelengths) != BYONOY_ERROR_NO_ERROR ||
byonoy_abs96_get_available_wavelengths(handle, wavelengths) != BYONOY_ERROR_NO_ERROR ||
wavelengths->wavelength_count == 0) {
printf("wavelength query failed\n");
goto done;
}
if (byonoy_create_abs96_single_measurement_config(&config) != BYONOY_ERROR_NO_ERROR ||
byonoy_create_abs96_single_measurement_result(&result) != BYONOY_ERROR_NO_ERROR) {
printf("allocation failed\n");
goto done;
}
config->sample_wavelength = wavelengths->wavelengths[0];
config->reference_wavelength = 0; /* 0 = no reference wavelength */
config->rapid_mode = false;
printf("remove any plate...\n"); fflush(stdout);
if (!wait_for_slot(handle, BYONOY_SLOT_EMPTY, 120)) { printf("slot not empty\n"); goto done; }
rc = byonoy_abs96_initialize_single_measurement(handle, config);
if (rc != BYONOY_ERROR_NO_ERROR) { printf("initialise failed: 0x%04x\n", rc); goto done; }
printf("insert the plate...\n"); fflush(stdout);
if (!wait_for_slot(handle, BYONOY_SLOT_OCCUPIED, 120)) {
printf("no plate inserted\n");
rc = BYONOY_ERROR_UNKNOWN_ERROR;
goto done;
}
sleep_ms(1000); /* let the plate settle */
rc = byonoy_abs96_single_measure(handle, config, result);
if (rc == BYONOY_ERROR_NO_ERROR) {
printf("OD at %u nm:\n", (unsigned)config->sample_wavelength);
for (int i = 0; i < 96; ++i) {
printf("%7.3f", result->value[i]);
if ((i + 1) % 12 == 0) printf("\n");
}
} else {
printf("measurement failed: 0x%04x\n", rc);
}
done:
if (result) byonoy_free_abs96_single_measurement_result(result);
if (config) byonoy_free_abs96_single_measurement_config(config);
if (wavelengths) byonoy_free_abs96_wavelengths(wavelengths);
if (status) byonoy_free_device_status(status);
byonoy_free_device(handle);
return rc == BYONOY_ERROR_NO_ERROR ? 0 : 1;
}
```
The rules behind it — wavelengths per unit, initialisation per handle and per
wavelength, the plate check — are in the
[Python document's absorbance section](SDK_PYTHON.md#absorbance); they apply to
C unchanged. An Absorbance One works the same way with
`byonoy_absone_initialize_measurement` and `byonoy_absone_measure`, but cannot
sense its slot; check `byonoy_absone_is_initialized` before measuring, because
on a faulty device the measure can block indefinitely.
### Bundled examples
`sdk/examples/C` holds compilable references, one directory each with a
`main.c` and a `CMakeLists.txt`:
| Path | Executable | Shows |
|---|---|---|
| `examples/C/device-info` | `device-information` | open, read information, close |
| `examples/C/lum96-measurement` | `lum96-measurement` | 96-well luminescence |
| `examples/C/abs96-multi-measurement` | `abs96-multi-wavelength` | multi-wavelength absorbance |
| `examples/C/absone-measurement` | `absone-measurement` | single-cuvette absorbance |
| `examples/C/device-update` | `device-update` | firmware update — **read before running**, see below |
They are references more than finished programs: they wait a fixed time for a
plate instead of checking the slot, and print return codes in decimal. Build
one with CMake:
```bash
cmake -S sdk/examples/C/device-info -B build-example
cmake --build build-example
./build-example/device-information
```
On Windows, add a generator — CMake otherwise looks for Visual Studio's
`nmake` — and copy the DLLs next to the result. Delete the build directory
after a failed configure; CMake caches the failed choice:
```powershell
cmake -S sdk/examples/C/device-info -B build-example -G Ninja
cmake --build build-example
Copy-Item sdk\bin\*.dll build-example\
.\build-example\device-information.exe
```
Known problems in the v2026.09.1 examples:
- **The binaries only run from the build directory.** CMake gives them an
absolute RPATH into the SDK, so they stop working when either is moved, and
`cmake --install` strips it entirely — the installed copy cannot find the
library. The default install location is the SDK folder itself. For a
binary you keep, use the relative RPATH from the `cc` lines above.
- **`absone-measurement` does not configure**: its `CMakeLists.txt` installs a
target named `abs96-multi-wavelength`. Change that line to
`install(TARGETS absone-measurement …)`.
- **`device-update` flashes a fixed file**, `./abs96auto-update.byoup`, onto
the first device it finds that supports updates — whatever its type. Adapt
it before running it.
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.