C SDK Usage Guide
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.

Installation instructions

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.
1
#include <stdio.h>
2
#include <stdlib.h>
3
#include <string.h>
4
#include <time.h>
5
#include "sensor_aq.h"
6
#include "sensor_aq_mbedtls_hs256.h"
7
8
int main() {
9
// context to sign data, this object is instantiated below
10
sensor_aq_signing_ctx_t signing_ctx;
11
12
// use HMAC-SHA256 signatures, signed with Mbed TLS
13
sensor_aq_mbedtls_hs256_ctx_t hs_ctx;
14
// initialize the Mbed TLS context which also instantiates signing_ctx
15
sensor_aq_init_mbedtls_hs256_context(&signing_ctx, &hs_ctx, "my-hmac-sha256-key");
16
17
// set up the sensor acquisition context
18
sensor_aq_ctx ctx = {
19
// the SDK requires a single buffer, and does not do any dynamic allocation
20
{ (unsigned char*)malloc(1024), 1024 },
21
22
// pass in the signing context
23
&signing_ctx,
24
25
// pointers to fwrite and fseek - note that these are pluggable so you
26
// can work with them on non-POSIX systems too. See the Porting Guide below.
27
&fwrite,
28
&fseek,
29
// if you set the time function this will add the 'iat' (issued at) field to the header
30
// you can set this pointer to NULL for device that don't have an accurate clock (not recommended)
31
&time
32
};
33
34
// payload header
35
sensor_aq_payload_info payload = {
36
// unique device ID (optional),
37
// set this to e.g. MAC address or device EUI **if** your device has one
38
"ac:87:a3:0a:2d:1b",
39
// device type (required), use the same device type for similar devices
40
"DISCO-L475VG-IOT01A",
41
// how often new data is sampled in ms. (100Hz = every 10 ms.)
42
// (note: this is a float)
43
10,
44
// the axes which you'll use.
45
// the units field needs to comply to SenML units
46
// (see https://www.iana.org/assignments/senml/senml.xhtml)
47
{ { "accX", "m/s2" }, { "accY", "m/s2" }, { "accZ", "m/s2" } }
48
};
49
50
// place to write our data
51
FILE *file = fopen("encoded.cbor", "w+");
52
53
// initialize the context, this verifies that all requirements are present.
54
// it also writes the initial CBOR structure.
55
int res;
56
res = sensor_aq_init(&ctx, &payload, file);
57
if (res != AQ_OK) {
58
printf("sensor_aq_init failed (%d)\n", res);
59
return 1;
60
}
61
62
// Periodically call `sensor_aq_add_data` to append data
63
// (according to the frequency in the payload header)
64
float values[][3] = {
65
{ -9.81, 0.03, 1.21 },
66
{ -9.83, 0.04, 1.28 },
67
{ -9.12, 0.03, 1.23 },
68
{ -9.14, 0.01, 1.25 }
69
};
70
for (size_t ix = 0; ix < sizeof(values) / sizeof(values[0]); ix++) {
71
res = sensor_aq_add_data(&ctx, values[ix], 3);
72
if (res != AQ_OK) {
73
printf("sensor_aq_add_data failed (%d)\n", res);
74
return 1;
75
}
76
}
77
78
// when you're done call `sensor_aq_finish`
79
// this will calculate the finalized signature
80
// and close the CBOR file
81
res = sensor_aq_finish(&ctx);
82
if (res != AQ_OK) {
83
printf("sensor_aq_finish failed (%d)\n", res);
84
return 1;
85
}
86
87
// this would be a good moment to upload 'encoded.cbor'
88
// to the Ingestion Service using your HTTP library of choice
89
90
// for convenience we'll print the encoded file.
91
// you can paste the output in http://cbor.me to decode
92
printf("Encoded file:\n");
93
94
// Print the content of the file here:
95
fseek(file, 0, SEEK_END);
96
size_t len = ftell(file);
97
uint8_t *buffer = (uint8_t*)malloc(len);
98
99
fseek(file, 0, SEEK_SET);
100
fread(buffer, len, 1, file);
101
102
for (size_t ix = 0; ix < len; ix++) {
103
printf("%02x ", buffer[ix]);
104
}
105
printf("\n");
106
}
Copied!
This example is also available in the SDK, and can be built with any modern C compiler:
1
$ cd ingestion-sdk-c/example
2
$ make
3
$ ./sensor-aq-test
4
a3 69 70 72 6f 74 65 63 74 65 64 a3 63 76 65 72 62 76 31 63 61 6c 67 65 48 53 32 35 36 63 69 61 74 1a 5d 8d f9 06 69 73 69 67 6e 61 74 75 72 65 78 40 63 30 35 62 66 66 36 34 33 39 66 37 39 62 66 32 31 31 38 63 36 34 36 66 36 64 61 30 65 63 66 61 38 32 35 62 33 33 38 37 37 33 66 66 62 35 39 63 37 66 64 34 36 61 38 34 38 64 66 64 37 61 63 37 67 70 61 79 6c 6f 61 64 a5 6b 64 65 76 69 63 65 5f 6e 61 6d 65 71 61 63 3a 38 37 3a 61 33 3a 30 61 3a 32 64 3a 31 62 6b 64 65 76 69 63 65 5f 74 79 70 65 73 44 49 53 43 4f 2d 4c 34 37 35 56 47 2d 49 4f 54 30 31 41 6b 69 6e 74 65 72 76 61 6c 5f 6d 73 0a 67 73 65 6e 73 6f 72 73 83 a2 64 6e 61 6d 65 64 61 63 63 58 65 75 6e 69 74 73 64 6d 2f 73 32 a2 64 6e 61 6d 65 64 61 63 63 59 65 75 6e 69 74 73 64 6d 2f 73 32 a2 64 6e 61 6d 65 64 61 63 63 5a 65 75 6e 69 74 73 64 6d 2f 73 32 66 76 61 6c 75 65 73 9f 83 fa c1 1c f5 c3 fa 3c f5 c2 8f fa 3f 9a e1 48 83 fa c1 1d 47 ae fa 3d 23 d7 0a fa 3f a3 d7 0a 83 fa c1 11 eb 85 fa 3c f5 c2 8f fa 3f 9d 70 a4 83 fa c1 12 3d 71 fa 3c 23 d7 0a f9 3d 00 ff
Copied!
You can paste the output into cbor.me to decode.

Signing contexts

If you are using a different TLS library you can implement a custom signing context. Here's an example:
1
/**
2
* Example none signing context
3
*/
4
#include <string.h>
5
#include "sensor_aq.h"
6
7
// initialization, set up your TLS library here
8
static int sensor_aq_signing_none_init(sensor_aq_signing_ctx_t *aq_ctx) {
9
// you can use `aq_ctx->ctx` to store a reference to your internal TLS context
10
// this value will be present in the other calls
11
12
// return code 0 means OK here, any other value means error
13
return 0;
14
}
15
16
// called whenever there is new data
17
static int sensor_aq_signing_none_update(sensor_aq_signing_ctx_t *aq_ctx, const uint8_t *buffer, size_t buffer_size) {
18
// update the state of your internal TLS context
19
// you can get your context back through `aq_ctx->ctx`
20
21
return 0;
22
}
23
24
// called when there is no more data, calculate the finished signature
25
static int sensor_aq_signing_none_finish(sensor_aq_signing_ctx_t *aq_ctx, uint8_t *buffer) {
26
// the `buffer` field will be equal to `aq_ctx->signature_length`
27
28
// signature will always be zero
29
memset(buffer, 0, aq_ctx->signature_length);
30
return 0;
31
}
32
33
/**
34
* construct a new signing context for none security
35
* **NOTE:** This will provide zero verification for your data and data might be rejected by your provider
36
*
37
* @param aq_ctx An empty signing context (can declare it without arguments)
38
*/
39
void sensor_aq_init_none_context(sensor_aq_signing_ctx_t *aq_ctx) {
40
// alg field in the header, please adhere to the JWS specification
41
aq_ctx->alg = "none";
42
// length of the signature
43
aq_ctx->signature_length = 1;
44
// internal reference to a context, e.g. the TLS context you created
45
aq_ctx->ctx = NULL;
46
47
// wire functions to the signing context
48
aq_ctx->init = sensor_aq_signing_none_init;
49
aq_ctx->update = sensor_aq_signing_none_update;
50
aq_ctx->finish = sensor_aq_signing_none_finish;
51
}
Copied!

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:
1
static int my_ctx_set_protected(struct sensor_aq_signing_ctx *aq_ctx, QCBOREncodeContext *cbor_ctx) {
2
QCBOREncode_AddInt64ToMap(cbor_ctx, "exp", time(NULL) + 3600); // expires an hour from now
3
4
return 0;
5
}
6
7
// during initialization
8
aq_ctx->set_protected = &my_ctx_set_protected;
Copied!

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:
1
// Override the stream
2
#define EI_SENSOR_AQ_STREAM memory_stream_t
3
4
// Holder for the stream
5
typedef struct {
6
uint8_t buffer[2048];
7
size_t length;
8
size_t current_position;
9
} memory_stream_t;
10
11
// fwrite function for the stream
12
size_t ms_fwrite(const void *ptr, size_t size, size_t count, memory_stream_t *stream) {
13
memcpy(stream->buffer + stream->current_position, ptr, size * count);
14
stream->current_position += size * count;
15
16
if (stream->current_position > stream->length) {
17
stream->length = stream->current_position;
18
}
19
20
return count;
21
}
22
23
// set current position in the stream
24
int ms_fseek(memory_stream_t *stream, long int offset, int origin) {
25
if (origin == 0 /* SEEK_SET */) {
26
stream->current_position = offset;
27
}
28
else if (origin == 1 /* SEEK_CUR */) {
29
stream->current_position += offset;
30
}
31
else if (origin == 2 /* SEEK_END */) {
32
stream->current_position = stream->length + offset;
33
}
34
// @todo: do a boundary check here
35
return 0;
36
}
37
38
int main() {
39
// Set up the sensor acquisition basic context
40
sensor_aq_ctx ctx = {
41
{ (unsigned char*)malloc(1024), 1024 },
42
&signing_ctx,
43
// custom fwrite / fseek
44
&ms_fwrite,
45
&ms_fseek,
46
NULL // no time on this system
47
};
48
49
// Place to write our data.
50
memory_stream_t stream;
51
stream.length = 0;
52
stream.current_position = 0;
53
54
sensor_aq_init(&ctx, &payload, &stream);
55
56
// ...
Copied!