12 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
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/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:
# 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, 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
#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/freepair. Callcreatefirst,freeexactly once, and never free something you did not create. - Functions returning
int,floatorboolthrough a pointer allocate nothing. byonoy_available_devices()allocates a list; release it withbyonoy_free_available_devices()once you have opened what you need. 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 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"); 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 (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.
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:
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:
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.