Quickstart: C
Get from an empty main() to detections, events and a correctly-handled licence failure. This is the path camera firmware takes.
core/include/xcaicx/xcaicx.h is the entire supported integration surface. If something is not in that header, it is not part of the contract and it will move.
1. Link it#
cmake -S core -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -jThat gives you libxcaicx.so (or .dylib / .dll) plus the header. In your own build:
find_package(OpenSSL REQUIRED)
target_include_directories(my_firmware PRIVATE ${XCAICX_ROOT}/core/include)
target_link_libraries(my_firmware PRIVATE xcaicx OpenSSL::Crypto)For a static link into a single firmware image, and for cross-compiling to your board, see Cross-compiling.
2. Create an engine#
Every configuration struct starts with xcaicx_config_init. It is not optional — it is what stamps struct_size, and struct_size is what lets a binary you compile today keep working against a future SDK.
#include "xcaicx/xcaicx.h"
#include <stdio.h>
int main(void) {
xcaicx_config cfg;
xcaicx_config_init(&cfg); /* always first */
cfg.license_path = "/etc/xcaicx/unit.token";
cfg.state_dir = "/var/lib/xcaicx"; /* must survive reboot */
cfg.modules = XCAICX_MODULE_DETECT | XCAICX_MODULE_PPE;
cfg.log_level = XCAICX_LOG_INFO;
xcaicx_engine *engine = NULL;
xcaicx_status st = xcaicx_engine_create(&cfg, &engine);
if (st != XCAICX_OK) {
fprintf(stderr, "xcaicx: %s (%s)\n",
xcaicx_status_str(st), xcaicx_last_error());
return 1;
}
...
}xcaicx_last_error() returns a thread-local human-readable detail for the last failing call on this thread. It is written to be read by a field technician — put it in your diagnostics page verbatim.
Careful
state_dir must be writable and survive reboot. It holds the activation record, the unit token, the clock high-water mark and the usage counters. Pointing it at /tmp means every reboot re-activates — burning a unit from your batch — and loses usage. See state_dir requirements.
3. Check what the licence actually allows#
Do this before you open a stream. It tells you the SKU, the entitlements, the caps, and how long you have.
xcaicx_license_info info;
info.struct_size = sizeof info;
if (xcaicx_engine_license_info(engine, &info) == XCAICX_OK) {
printf("sku=%s state=%d streams<=%d expires_in=%llds\n",
info.sku, (int)info.state, info.max_streams,
(long long)info.seconds_remaining);
if (info.state == XCAICX_LIC_GRACE) {
/* Expired but still running. Surface it loudly so the customer
renews before the hard stop. */
raise_maintenance_warning(info.seconds_remaining);
}
}4. Open a stream#
xcaicx_stream_config sc;
xcaicx_stream_config_init(&sc);
sc.name = "sensor-0"; /* used in logs and metering attribution */
sc.modules = 0; /* 0 = all modules the engine has */
sc.detect_threshold = 0.35f;
sc.detect_every_n = 1;
xcaicx_stream *stream = NULL;
if (xcaicx_stream_open(engine, &sc, &stream) != XCAICX_OK) {
fprintf(stderr, "stream: %s\n", xcaicx_last_error());
return 1;
}Opening more streams than the licence allows fails with XCAICX_ERR_LIMIT_EXCEEDED — the cap is max_streams from the token, not a config value you can raise.
5. Add zones and tripwires#
Coordinates are normalised to [0,1], so they survive a resolution change. A zone is a polygon; a line is directed, and crossing it from a to b is reported distinctly from b to a.
const xcaicx_point bay[] = {
{0.55f, 0.0f}, {1.0f, 0.0f}, {1.0f, 1.0f}, {0.55f, 1.0f}
};
int32_t zone_id = -1;
xcaicx_stream_add_zone(stream, "packing-bay", bay, 4,
/*dwell_seconds=*/30, &zone_id);
int32_t line_id = -1;
xcaicx_stream_add_line(stream, "doorway",
(xcaicx_point){0.5f, 0.0f},
(xcaicx_point){0.5f, 1.0f}, &line_id);Objects are tested by the midpoint of their bottom edge — the ground contact point — not the box centre. Using the centre makes a tall person "enter" a floor zone while still a metre outside it. See Zones, tripwires & events.
6. Feed frames#
Pass what your ISP already produces. NV12 and I420 are first-class.
/* in your ISP callback */
xcaicx_frame f = {
.struct_size = sizeof f,
.data = isp_buffer, /* borrowed — never freed by the SDK */
.data_len = isp_buffer_len,
.width = 1920,
.height = 1080,
.stride = isp_stride, /* 0 if tightly packed */
.format = XCAICX_PIX_NV12,
.pts_us = frame_pts,
.frame_id = seq++,
};
const xcaicx_result *res = NULL;
xcaicx_status ps = xcaicx_stream_process(stream, &f, &res);
if (ps == XCAICX_OK) {
for (size_t i = 0; i < res->detection_count; ++i) {
const xcaicx_detection *d = &res->detections[i];
printf("%s %.2f track=%lld\n",
d->class_name, d->score, (long long)d->track_id);
}
for (size_t i = 0; i < res->event_count; ++i) {
const xcaicx_event *e = &res->events[i];
printf("event %d zone=%s detail=%s\n",
(int)e->kind, e->zone_name ? e->zone_name : "-",
e->detail ? e->detail : "-");
}
}Note
res is owned by the stream and valid only until the next call on that stream. Copy anything you need to keep. The same rule applies to class_name, zone_name and detail — they are library-owned strings.
Do not convert to RGB before handing frames over. That doubles the memcpy cost on exactly the boards that can least afford it. Details in Feeding frames.
7. Handle each licensing failure separately#
This is the single most common integration mistake. Collapsing these into "license bad" means a field technician has to guess.
switch (ps) {
case XCAICX_OK:
break;
case XCAICX_ERR_LICENSE_MISSING:
ui_error("No licence installed. Run provisioning.");
break;
case XCAICX_ERR_ACTIVATION_REQUIRED:
ui_error("Camera not activated. Connect it to the network once.");
break;
case XCAICX_ERR_LICENSE_WRONG_DEVICE:
ui_error("Licence belongs to another camera. Was the SD card swapped?");
break;
case XCAICX_ERR_LICENSE_EXPIRED:
case XCAICX_ERR_LICENSE_REVOKED:
disable_analytics(); /* keep the video path alive */
raise_maintenance_alert();
break;
case XCAICX_ERR_LICENSE_CLOCK_ROLLBACK:
ui_error("System clock is behind. Check NTP or the RTC battery.");
break;
case XCAICX_ERR_ENTITLEMENT_DENIED:
ui_error("This module is not included in the installed licence.");
break;
case XCAICX_ERR_LIMIT_EXCEEDED:
ui_error("Stream or FPS limit reached for this licence.");
break;
case XCAICX_ERR_QUEUE_FULL:
/* Backpressure, not an error. Drop the frame. */
break;
default:
ui_error(xcaicx_status_str(ps));
}Do not
A camera that stops recording because a licence lapsed is a warranty claim. When analytics fail, keep the video path alive. The SDK is built so that is always possible — do not undo it in your integration.
8. Shut down cleanly#
xcaicx_stream_close(stream);
xcaicx_engine_destroy(engine); /* flushes metering to disk */Skipping xcaicx_engine_destroy loses the session's usage — which is unbilled revenue. Wire it to your shutdown path and to your signal handler.
Asynchronous variant#
xcaicx_stream_process is synchronous. For a pipeline that must not block the ISP thread, submit and poll instead:
xcaicx_status s = xcaicx_stream_submit(stream, &f);
if (s == XCAICX_ERR_QUEUE_FULL) {
/* Flow control. Drop this frame; do not retry in a tight loop. */
}
const xcaicx_result *res = NULL;
if (xcaicx_stream_poll(stream, /*timeout_ms=*/0, &res) == XCAICX_OK) {
handle(res);
}submit() converts on your thread and runs inference on a worker. A single stream must be driven from one thread at a time; the engine itself is safe to use from several.
Next#
- OEM integration — fingerprints, provisioning, the pre-ship checklist
- C ABI reference — every function, struct and enum
- Status codes — the full table with causes and fixes