From f7850e9043487f1577222eaf280101145696885d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Delf=20Neum=C3=A4rker?= Date: Thu, 1 Oct 2026 15:41:47 +0200 Subject: [PATCH] Expand native SDK info and examples --- README.md | 11 ++- SDK_NATIVE.md | 264 ++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 245 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index a4cbdf2..2a78aba 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,9 @@ so it differs between units. initialisation had been attempted. Treat it as a rule: call `absone_measure` only when the device status is `OK` and `absone_is_initialized` reports `True` (see [Device status and device error](#device-status-and-device-error)). +- **The library's own wait for an absorbance result is 110 s** by default, after + which the call returns an error. The device can extend that wait, so it is + not a guaranteed bound. **Stopping a blocked measurement is a last resort.** The only way out of a call that does not return is to end the process; the library's own internal waits @@ -311,7 +314,8 @@ These numbers are **not** the library's return codes listed under they coincide (`0x8001` there is `MEASUREMENT_SLOT_NOT_EMPTY`). **What the device actually reported** is not available through the API. It -appears only in the protocol log (`enable_logging`), as a text ID such as +appears only in the protocol log (`byonoy_enable_logging` in C, `enable_logging` in +Python — it goes to the process's standard output), as a text ID such as `com.byonoy-AbsOne-MIN_LIGHT_ERROR`. For the two absorbance devices, these mean: | Text ID | Meaning | @@ -471,6 +475,11 @@ Recovery is a device reboot. The internal variant can do this from software (`reboot()`); **the public variant cannot** — unplug the device and plug it back in. Either way it re-enumerates within a few seconds and you open it again. +If the device is stuck again straight after a power cycle, check whether a +process was ended in the middle of a call in between — a watchdog killing a +slow first call is enough to put it back. Power-cycle it again, and give the +first calls time to complete. + > **Report it if it recurs.** A one-off on an engineering or pre-production unit > is not remarkable. A device that needs rebooting repeatedly, or one on which a > particular mode fails consistently, is a fault worth reporting to Byonoy diff --git a/SDK_NATIVE.md b/SDK_NATIVE.md index 778ec15..8a4d34b 100644 --- a/SDK_NATIVE.md +++ b/SDK_NATIVE.md @@ -23,6 +23,20 @@ sha256sum --ignore-missing -c SHA256SUMS # macOS: shasum -a 256 --ignore-mi 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-.zip`. If you have been given access to the internal repository's releases, it is attached to each release there and @@ -56,20 +70,48 @@ 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/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. `bin/` holds the Windows runtime only — on macOS and +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 @@ -77,7 +119,8 @@ library at run time.** The shipped library's install name is your own and you get a clean compile followed by: ``` -dyld: Library not loaded: @rpath/libbyonoy_device_library.dylib +dyld[…]: Library not loaded: @rpath/libbyonoy_device_library.dylib + Referenced from: … demo Reason: no LC_RPATH's found ``` @@ -102,12 +145,21 @@ 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. +On **Windows** there is no RPATH. Compile, then **copy all five DLLs from +`sdk\bin` next to the executable** — they travel with it: -The examples below use `uint32_t` and `true`, which the SDK header provides on a -current compiler. Add `#include ` and `` if yours is older -than C23. +```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. --- @@ -122,8 +174,7 @@ int main(void) { 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 */ + printf("no device\n"); /* nothing to free */ return 1; } @@ -166,8 +217,9 @@ and `type` as a `byonoy_device_types` enum. 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_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 @@ -180,7 +232,7 @@ and `type` as a `byonoy_device_types` enum. ## 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: +complete luminescence program, so it can be compiled as it stands: ```c #include "byonoy_device_library.h" @@ -190,7 +242,7 @@ 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; } + if (count == 0) { printf("no device\n"); return 1; } byonoy_device_handle_t handle; byonoy_error_code rc = byonoy_open_device(devices, &handle); @@ -246,23 +298,152 @@ 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 +#if defined(_WIN32) +#include +static void sleep_ms(unsigned ms) { Sleep(ms); } +#else +#include +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/` holds compilable references, one directory each with a `main.c` -or `main.cpp` and a `CMakeLists.txt`: +`sdk/examples/C` holds compilable references, one directory each with a +`main.c` 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) | +| 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 build with CMake, and CMake sets the RPATH for you — which makes this the -safer route than the `cc` line above: +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 @@ -270,6 +451,31 @@ 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: