Compare commits
2
Commits
v2026.09.1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc4b2ace67 | ||
|
|
f7850e9043
|
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user