Expand native SDK info and examples

This commit is contained in:
2026-10-01 15:41:47 +02:00
parent c10d856129
commit f7850e9043
2 changed files with 245 additions and 30 deletions
+10 -1
View File
@@ -203,6 +203,9 @@ so it differs between units.
initialisation had been attempted. Treat it as a rule: call `absone_measure` initialisation had been attempted. Treat it as a rule: call `absone_measure`
only when the device status is `OK` and `absone_is_initialized` reports only when the device status is `OK` and `absone_is_initialized` reports
`True` (see [Device status and device error](#device-status-and-device-error)). `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 **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 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`). they coincide (`0x8001` there is `MEASUREMENT_SLOT_NOT_EMPTY`).
**What the device actually reported** is not available through the API. It **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: `com.byonoy-AbsOne-MIN_LIGHT_ERROR`. For the two absorbance devices, these mean:
| Text ID | Meaning | | 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 (`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. 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 > **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 > 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 > particular mode fails consistently, is a fault worth reporting to Byonoy
+235 -29
View File
@@ -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 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 **The internal native SDK is available on request**, as
`byonoy-devices-internal-sdk-<version>.zip`. If you have been given access to `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 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.so Linux
sdk/lib/libbyonoy_device_library.dylib macOS sdk/lib/libbyonoy_device_library.dylib macOS
sdk/bin/libbyonoy_device_library.dll Windows runtime sdk/bin/libbyonoy_device_library.dll Windows runtime
sdk/lib/libbyonoy_device_library.dll.a Windows import library sdk/bin/libhidapi.dll, libgcc_s_seh-1.dll,
sdk/lib/libhidapi* dependency, ships alongside libstdc++-6.dll, libwinpthread-1.dll Windows dependencies, ship alongside
sdk/examples/C, sdk/examples/C++ compilable references 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 sdk/third-party-licenses/ notices for bundled dependencies
``` ```
The internal archive is the same with `_internal` appended to every library 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/`. Linux everything you need is in `lib/`.
--- ---
## 3. Build and run ## 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 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 library at run time.** The shipped library's install name is
`@rpath/libbyonoy_device_library.dylib`, and its own RPATH entries only cover how `@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: 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 Reason: no LC_RPATH's found
``` ```
@@ -102,12 +145,21 @@ DYLD_LIBRARY_PATH=sdk/lib ./demo # macOS
LD_LIBRARY_PATH=sdk/lib ./demo # Linux LD_LIBRARY_PATH=sdk/lib ./demo # Linux
``` ```
On Windows, put `sdk/bin` on `PATH`; the DLLs there must travel with the On **Windows** there is no RPATH. Compile, then **copy all five DLLs from
executable. `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 ```powershell
current compiler. Add `#include <stdint.h>` and `<stdbool.h>` if yours is older gcc -I sdk/include main.c -L sdk/lib -lbyonoy_device_library -o demo.exe
than C23. 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; uint32_t count = 0;
byonoy_available_devices(&devices, &count); /* returns void */ byonoy_available_devices(&devices, &count); /* returns void */
if (count == 0) { if (count == 0) {
printf("no device\n"); printf("no device\n"); /* nothing to free */
byonoy_free_available_devices(); /* allocated even when empty */
return 1; return 1;
} }
@@ -166,8 +217,9 @@ and `type` as a `byonoy_device_types` enum.
create. create.
- Functions returning `int`, `float` or `bool` through a pointer allocate - Functions returning `int`, `float` or `bool` through a pointer allocate
nothing. nothing.
- `byonoy_available_devices()` allocates a list; release it with - `byonoy_available_devices()` hands out a list owned by the library; release
`byonoy_free_available_devices()` once you have opened what you need. The 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 `byonoy_device_t*` entries are invalid afterwards — the **handle** is what
stays valid. stays valid.
- `byonoy_free_device(handle)` closes the device. The handle is invalid after - `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 ## 5. Taking a measurement
Gate on the predicate, `create` the config and result, measure, free both. A 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 ```c
#include "byonoy_device_library.h" #include "byonoy_device_library.h"
@@ -190,7 +242,7 @@ int main(void) {
byonoy_device_t* devices = NULL; byonoy_device_t* devices = NULL;
uint32_t count = 0; uint32_t count = 0;
byonoy_available_devices(&devices, &count); 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_device_handle_t handle;
byonoy_error_code rc = byonoy_open_device(devices, &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 [Python document](SDK_PYTHON.md#which-modality-does-this-device-support) is also
the C capability table, with `byonoy_` prefixes. 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 <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](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 ### Bundled examples
`sdk/examples/` holds compilable references, one directory each with a `main.c` `sdk/examples/C` holds compilable references, one directory each with a
or `main.cpp` and a `CMakeLists.txt`: `main.c` and a `CMakeLists.txt`:
| Path | Shows | | Path | Executable | Shows |
|---|---| |---|---|---|
| `examples/C/device-info` | open, read information, close | | `examples/C/device-info` | `device-information` | open, read information, close |
| `examples/C/lum96-measurement` | 96-well luminescence | | `examples/C/lum96-measurement` | `lum96-measurement` | 96-well luminescence |
| `examples/C/abs96-multi-measurement` | multi-wavelength absorbance | | `examples/C/abs96-multi-measurement` | `abs96-multi-wavelength` | multi-wavelength absorbance |
| `examples/C/absone-measurement` | single-cuvette absorbance | | `examples/C/absone-measurement` | `absone-measurement` | single-cuvette absorbance |
| `examples/C/device-update` | firmware update | | `examples/C/device-update` | `device-update` | firmware update — **read before running**, see below |
| `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 They are references more than finished programs: they wait a fixed time for a
safer route than the `cc` line above: plate instead of checking the slot, and print return codes in decimal. Build
one with CMake:
```bash ```bash
cmake -S sdk/examples/C/device-info -B build-example cmake -S sdk/examples/C/device-info -B build-example
@@ -270,6 +451,31 @@ cmake --build build-example
./build-example/device-information ./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 Against the **internal** archive their `find_library` call fails, because they
look for the public library name while the internal archive ships look for the public library name while the internal archive ships
`libbyonoy_device_library_internal.*`. Point the cache variable at the real file: `libbyonoy_device_library_internal.*`. Point the cache variable at the real file: