Add SDK guide for the Byonoy device library
This commit is contained in:
+311
@@ -0,0 +1,311 @@
|
||||
# Byonoy Device Library — Native SDK
|
||||
|
||||
Read the **[SDK guide](README.md)** 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 native SDK is available on request, in either variant.** It is not
|
||||
published anywhere you can fetch it from without Byonoy providing it. Ask for
|
||||
the variant you need and you will be given an archive,
|
||||
`byonoy-devices-public-sdk-<version>.zip` or
|
||||
`byonoy-devices-internal-sdk-<version>.zip`.
|
||||
|
||||
If you have been given access to the internal repository's releases, the archive
|
||||
is attached to each release and can be downloaded with your token:
|
||||
|
||||
```bash
|
||||
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-public-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`). Public archives are attached to final tags; a recent pre-release tag
|
||||
carries internal artifacts only. 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:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```c
|
||||
#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:
|
||||
|
||||
```c
|
||||
#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](README.md#well-order) (`value[0]` is A1, `value[12]` is B1), as
|
||||
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.
|
||||
|
||||
### 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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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](README.md#1-which-variant-public-or-internal)
|
||||
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](SDK_PYTHON.md#what-it-adds); 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.
|
||||
Reference in New Issue
Block a user