20 KiB
Byonoy Device Library — Native SDK
Read the SDK guide 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 carries it as byonoy-devices-public-sdk-<version>.zip,
next to a SHA256SUMS file. No account or token is needed:
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:
$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:
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:
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:
# 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:
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:
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
#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/freepair. Callcreatefirst,freeexactly once, and never free something you did not create. - Functions returning
int,floatorboolthrough a pointer allocate nothing. byonoy_available_devices()hands out a list owned by the library; release it withbyonoy_free_available_devices()once you have opened what you need (an empty list needs no release, though releasing it is harmless). Thebyonoy_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 returnBYONOY_ERROR_INVALID_ARGUMENT(0x0003) — notDEVICE_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:
#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 (value[0] is A1, value[12] is B1), as
is selected_wells. The capability table in the
Python document 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:
#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; 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:
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:
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 --installstrips 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 thecclines above. absone-measurementdoes not configure: itsCMakeLists.txtinstalls a target namedabs96-multi-wavelength. Change that line toinstall(TARGETS absone-measurement …).device-updateflashes 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:
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 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; 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.