Firmware Integration
How to include the Luviner model in your firmware project and run inference on your MCU.
What you receive
After compilation, you download a ZIP containing two files:
luviner_v3_model.a — static library containing the trained model (UID-locked)
luviner_v3_model.h — header file with the inference API and model constants
API overview
The header defines model constants and exposes the inference functions. All inputs and outputs use integer arithmetic (int16_t) — no floating point needed on the MCU.
#include <stdint.h>
#define NC3_N_INPUTS 8
#define NC3_N_OUTPUTS 4
int nc3_activate(uint64_t device_uid);
void nc3_init(void);
int nc3_predict(const int16_t inputs[NC3_N_INPUTS]);
int nc3_stream_predict(const int16_t inputs[NC3_N_INPUTS]);
void nc3_stream_reset(void);
Converting sensor readings
The model expects inputs as fixed-point integers. Normalize your sensor values the same way your CSV training data was structured, then multiply by 256:
static inline int16_t float_to_fixed(float value) {
int32_t q = (int32_t)(value * 256.0f);
if (q > 32767) return 32767;
if (q < -32768) return -32768;
return (int16_t)q;
}
Basic usage example
#include "luviner_v3_model.h"
#include <stdio.h>
const char *classes[] = {"normal", "bearing_fault", "misalignment", "imbalance"};
#define MY_DEVICE_UID 0x1234567890ABCDEFULL
int main(void) {
if (!nc3_activate(MY_DEVICE_UID)) {
printf("License check failed\n");
return 1;
}
nc3_init();
int16_t inputs[NC3_N_INPUTS];
inputs[0] = float_to_fixed(0.12f);
inputs[1] = float_to_fixed(0.98f);
inputs[2] = float_to_fixed(-0.05f);
int prediction = nc3_predict(inputs);
printf("Predicted: %s\n", classes[prediction]);
return 0;
}
Build integration
ARM Cortex-M (Makefile)
LIBS += luviner_v3_model.a
INCLUDES += -I./luviner/
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -Os \
-I./luviner/ main.c -L./luviner/ -lluviner_v3_model -o firmware.elf
ESP-IDF (CMakeLists.txt)
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "." "luviner"
)
target_link_libraries(${COMPONENT_LIB} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/luviner/luviner_v3_model.a"
)
Arduino
#include "luviner_v3_model.h"
void setup() {
Serial.begin(115200);
nc3_activate(0x1234567890ABCDEFULL);
nc3_init();
}
void loop() {
int16_t data[NC3_N_INPUTS];
data[0] = (int16_t)((analogRead(A0) * 256L) / 1023);
data[1] = (int16_t)((analogRead(A1) * 256L) / 1023);
int cls = nc3_predict(data);
Serial.println(cls);
delay(100);
}
Streaming (time-series) inference
For time-series data (vibration monitoring, ECG, etc.), use nc3_stream_predict() instead of nc3_predict(). The model maintains internal state between calls, allowing it to detect temporal patterns across consecutive readings.
nc3_stream_reset();
while (1) {
read_and_convert_sensors(inputs);
int cls = nc3_stream_predict(inputs);
if (cls != CLASS_NORMAL) {
trigger_alert(cls);
}
sleep_ms(100);
}
No windowing needed. Unlike traditional models that require collecting a window of N samples, Luviner processes each reading individually while retaining temporal context. This reduces latency and memory usage.
UID verification
The compiled library is bound to specific chip UIDs. Call nc3_activate(device_uid) at startup with your chip's unique ID (read from the MCU's UID register). If the UID is not in the authorized list, the function returns 0 and all inference functions will return -1.
To add more devices to your deployment, add their UIDs in the Luviner dashboard and recompile.
Reading the chip UID. Each MCU family has a specific register for the unique ID: STM32 uses 0x1FFF7A10, ESP32 uses esp_efuse_mac_get_default(), nRF uses NRF_FICR->DEVICEID. Consult your MCU datasheet for the exact location.