Files
byonoy_devices_sdk/SDK_NATIVE.md
T

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/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:

#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 --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:

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.