The C Ingestion SDK is a portable header-only library written in C99 for data collection on embedded devices. It's designed to reliably store sampled data from sensors at a high frequency in very little memory. On top of this it allows cryptographic signing of the data when sampling is complete. Data can be stored on a POSIX file system, in memory, or on a raw block device.
Copy the inc folder to your project. This contains all headers and source files.
Usage
The following application:
Initializes the library.
Sets up the Mbed TLS signing context with the key my-hmac-sha256-key.
Creates a file with three axes (accX, accY, accZ) and four readings.
It then prints out the CBOR buffer.
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<time.h>#include"sensor_aq.h"#include"sensor_aq_mbedtls_hs256.h"intmain() {// context to sign data, this object is instantiated belowsensor_aq_signing_ctx_t signing_ctx;// use HMAC-SHA256 signatures, signed with Mbed TLSsensor_aq_mbedtls_hs256_ctx_t hs_ctx;// initialize the Mbed TLS context which also instantiates signing_ctxsensor_aq_init_mbedtls_hs256_context(&signing_ctx,&hs_ctx,"my-hmac-sha256-key");// set up the sensor acquisition context sensor_aq_ctx ctx = {// the SDK requires a single buffer, and does not do any dynamic allocation { (unsignedchar*)malloc(1024),1024 },// pass in the signing context&signing_ctx,// pointers to fwrite and fseek - note that these are pluggable so you // can work with them on non-POSIX systems too. See the Porting Guide below.&fwrite,&fseek,// if you set the time function this will add the 'iat' (issued at) field to the header// you can set this pointer to NULL for device that don't have an accurate clock (not recommended)&time };// payload header sensor_aq_payload_info payload = {// unique device ID (optional), // set this to e.g. MAC address or device EUI **if** your device has one"ac:87:a3:0a:2d:1b",// device type (required), use the same device type for similar devices"DISCO-L475VG-IOT01A",// how often new data is sampled in ms. (100Hz = every 10 ms.)// (note: this is a float)10,// the axes which you'll use. // the units field needs to comply to SenML units // (see https://www.iana.org/assignments/senml/senml.xhtml) { { "accX","m/s2" }, { "accY","m/s2" }, { "accZ","m/s2" } } };// place to write our data FILE *file =fopen("encoded.cbor","w+");// initialize the context, this verifies that all requirements are present.// it also writes the initial CBOR structure.int res; res =sensor_aq_init(&ctx,&payload, file);if (res != AQ_OK) {printf("sensor_aq_init failed (%d)\n", res);return1; }// Periodically call `sensor_aq_add_data` to append data// (according to the frequency in the payload header)float values[][3] = { { -9.81,0.03,1.21 }, { -9.83,0.04,1.28 }, { -9.12,0.03,1.23 }, { -9.14,0.01,1.25 } };for (size_t ix =0; ix <sizeof(values) /sizeof(values[0]); ix++) { res =sensor_aq_add_data(&ctx, values[ix],3);if (res != AQ_OK) {printf("sensor_aq_add_data failed (%d)\n", res);return1; } }// when you're done call `sensor_aq_finish`// this will calculate the finalized signature // and close the CBOR file res =sensor_aq_finish(&ctx);if (res != AQ_OK) {printf("sensor_aq_finish failed (%d)\n", res);return1; }// this would be a good moment to upload 'encoded.cbor' // to the Ingestion Service using your HTTP library of choice// for convenience we'll print the encoded file. // you can paste the output in http://cbor.me to decodeprintf("Encoded file:\n");// Print the content of the file here:fseek(file,0, SEEK_END);size_t len =ftell(file);uint8_t*buffer = (uint8_t*)malloc(len);fseek(file,0, SEEK_SET);fread(buffer, len,1, file);for (size_t ix =0; ix < len; ix++) {printf("%02x ", buffer[ix]); }printf("\n");}
This example is also available in the SDK, and can be built with any modern C compiler:
If you are using a different TLS library you can implement a custom signing context. Here's an example:
/** * Example none signing context */#include<string.h>#include"sensor_aq.h"// initialization, set up your TLS library herestaticintsensor_aq_signing_none_init(sensor_aq_signing_ctx_t*aq_ctx) {// you can use `aq_ctx->ctx` to store a reference to your internal TLS context// this value will be present in the other calls// return code 0 means OK here, any other value means errorreturn0;}// called whenever there is new datastaticintsensor_aq_signing_none_update(sensor_aq_signing_ctx_t*aq_ctx,constuint8_t*buffer,size_t buffer_size) {// update the state of your internal TLS context// you can get your context back through `aq_ctx->ctx`return0;}// called when there is no more data, calculate the finished signaturestaticintsensor_aq_signing_none_finish(sensor_aq_signing_ctx_t*aq_ctx,uint8_t*buffer) {// the `buffer` field will be equal to `aq_ctx->signature_length`// signature will always be zeromemset(buffer,0,aq_ctx->signature_length);return0;}/** * construct a new signing context for none security * **NOTE:** This will provide zero verification for your data and data might be rejected by your provider * * @param aq_ctx An empty signing context (can declare it without arguments) */voidsensor_aq_init_none_context(sensor_aq_signing_ctx_t*aq_ctx) {// alg field in the header, please adhere to the JWS specificationaq_ctx->alg ="none";// length of the signatureaq_ctx->signature_length =1;// internal reference to a context, e.g. the TLS context you createdaq_ctx->ctx =NULL;// wire functions to the signing contextaq_ctx->init = sensor_aq_signing_none_init;aq_ctx->update = sensor_aq_signing_none_update;aq_ctx->finish = sensor_aq_signing_none_finish;}
Adding fields to the protected header
The signing context can also add new fields to the protected header, such as an expiration date for the token. We suggest to use the JWT Claim Names. You do this by registering a set_protected callback:
staticintmy_ctx_set_protected(struct sensor_aq_signing_ctx *aq_ctx, QCBOREncodeContext *cbor_ctx) {QCBOREncode_AddInt64ToMap(cbor_ctx,"exp", time(NULL) +3600); // expires an hour from nowreturn0;}// during initializationaq_ctx->set_protected =&my_ctx_set_protected;
Usage on non-POSIX systems
The storage layer is pluggable. You'll need to set the EI_SENSOR_AQ_STREAM macro to an object of your choice, and then implement fwrite() and fseek() methods to interact with the storage layer. If your system has a clock you can also implement the time() method. If not, the iat (issued at) field in the header will be omitted.
This is an example of using a memory-backed storage layer:
// Override the stream#defineEI_SENSOR_AQ_STREAMmemory_stream_t// Holder for the streamtypedefstruct {uint8_t buffer[2048];size_t length;size_t current_position;} memory_stream_t;// fwrite function for the streamsize_tms_fwrite(constvoid*ptr,size_t size,size_t count,memory_stream_t*stream) {memcpy(stream->buffer +stream->current_position, ptr, size * count);stream->current_position += size * count;if (stream->current_position >stream->length) {stream->length =stream->current_position; }return count;}// set current position in the streamintms_fseek(memory_stream_t*stream,longint offset,int origin) {if (origin ==0 /* SEEK_SET */) {stream->current_position = offset; }elseif (origin ==1 /* SEEK_CUR */) {stream->current_position += offset; }elseif (origin ==2 /* SEEK_END */) {stream->current_position =stream->length + offset; }// @todo: do a boundary check herereturn0;}intmain() {// Set up the sensor acquisition basic context sensor_aq_ctx ctx = { { (unsignedchar*)malloc(1024),1024 },&signing_ctx,// custom fwrite / fseek&ms_fwrite,&ms_fseek,NULL // no time on this system };// Place to write our data.memory_stream_t stream;stream.length =0;stream.current_position =0;sensor_aq_init(&ctx,&payload,&stream);// ...