2 Commits
Author SHA1 Message Date
nmrkr cc4b2ace67 Update SDK_PYTHON.md 2026-10-05 09:53:24 +00:00
nmrkr f7850e9043 Expand native SDK info and examples 2026-10-01 15:41:47 +02:00
3 changed files with 246 additions and 31 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`
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
+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
```
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-<version>.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 <stdint.h>` and `<stdbool.h>` 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 <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
`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:
+1 -1
View File
@@ -145,7 +145,7 @@ $tag = "cp" + (python -c "import sys; print(f'{sys.version_info.major}{sys.versi
Both filters matter — the ABI tag alone lists wheels for every platform. (The
PowerShell version also filters on the release; without that it lists every
older wheel too.) Switch
to a supported interpreter rather than fighting the resolver.
to a supported interpreter or submit a request to Byonoy including OS, CPU architecture and Python version.
---