Files
byonoy_devices_sdk/SDK_NATIVE.md
T

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

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