diff --git a/CMakeLists.txt b/CMakeLists.txt index d53e48db..6376b886 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ add_custom_target(uninstall ######################################################################## add_subdirectory(include) add_subdirectory(src) +add_subdirectory(tests) # use space-separation format for the pc file STRING(REPLACE ";" " " RTL433_PC_CFLAGS "${RTL433_PC_CFLAGS}") diff --git a/include/data.h b/include/data.h new file mode 100644 index 00000000..af77fa47 --- /dev/null +++ b/include/data.h @@ -0,0 +1,98 @@ +/* + * A general structure for extracting hierarchical data from the devices; + * typically key-value pairs, but allows for more rich data as well. + * + * Copyright (C) 2015 by Erkki Seppälä + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef INCLUDE_DATA_H_ +#define INCLUDE_DATA_H_ + +typedef enum { + DATA_DATA, /* pointer to data is stored */ + DATA_INT, /* pointer to integer is stored */ + DATA_DOUBLE, /* pointer to a double is stored */ + DATA_STRING, /* pointer to a string is stored */ + DATA_ARRAY, /* pointer to an array of values is stored */ + DATA_COUNT /* invalid */ +} data_type_t; + +typedef struct data_array { + int num_values; + data_type_t type; + void *values; +} data_array_t; + +typedef struct data { + char *key; + char *pretty_key; /* the name used for displaying data to user in with a nicer name */ + data_type_t type; + void *value; + struct data* next; /* chaining to the next element in the linked list; NULL indicates end-of-list */ +} data_t; + +/** Constructs a structured data object. + + Example: + data_make("key", "Pretty key", DATA_INT, 42, + "others", "More data", DATA_DATA, data_make("foo", DATA_DOUBLE, 42.0, NULL), + "zoom", NULL, data_array(2, DATA_STRING, (char*[]){"hello", "World"}), + "double", "Double", DATA_DOUBLE, 10.0/3, + NULL); + + Most of the time the function copies perhaps what you expect it to. Things + it copies: + - string contents for keys and values + - numerical arrays + - string arrays (copied deeply) + + Things it moves: + - recursive data_t* and data_array_t* values + + The rule is: if an object is boxed (look at the dmt structure in the data.c) + and it has a array_elementwise_import in the same structure, then it is + copied deeply. Otherwise, it is copied shallowly. + + @param key Name of the first value to put in. + @param pretty_key Pretty name for the key. Use "" if to omit pretty label for this field completely, + or NULL if to use key name for it. + @param type Type of the first value to put in. + @param ... The value of the first value to put in, follwed by the rest of the + key-type-values. The list is terminated with a NULL. + + @return A constructed data_t* object or NULL if there was a memory allocation error. +*/ +data_t *data_make(const char *key, const char *pretty_key, data_type_t type, ...); + +/** Constructs an array from given data of the given uniform type. + + @param ptr The contents pointed by the argument are copied in. + + @return The constructed data array object, typically placed inside a data_t or NULL + if there was a memory allocation error. +*/ +data_array_t *data_array(int num_values, data_type_t type, void *ptr); + +/** Releases a data array */ +void data_array_free(data_array_t *array); + +/** Prints a structured data object as JSON to the given stream */ +void data_print_json(data_t *data, FILE* file); + +/** Releases a structure object */ +void data_free(data_t *data); + +#endif // INCLUDE_DATA_H_ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d8357bbf..3742667d 100755 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -23,6 +23,7 @@ add_executable(rtl_433 pulse_demod.c pulse_detect.c util.c + data.c devices/silvercrest.c devices/rubicson.c devices/prologue.c @@ -60,6 +61,8 @@ add_executable(rtl_433 devices/generic_temperature_sensor.c ) +add_library(data data.c) + target_link_libraries(rtl_433 ${LIBRTLSDR_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} diff --git a/src/data.c b/src/data.c new file mode 100644 index 00000000..5d840b5e --- /dev/null +++ b/src/data.c @@ -0,0 +1,360 @@ +/* + * A general structure for extracting hierarchical data from the devices; + * typically key-value pairs, but allows for more rich data as well. + * + * Copyright (C) 2015 by Erkki Seppälä + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include "data.h" + +typedef void* (*array_elementwise_import_fn)(void*); +typedef void* (*array_element_release_fn)(void*); +typedef void* (*value_release_fn)(void*); + +typedef struct { + /* what is the element size when put inside an array? */ + int array_element_size; + + /* is the element boxed (ie. behind a pointer) when inside an array? + if it's not boxed ("unboxed"), json dumping function needs to make + a copy of the value beforehand, because the dumping function only + deals with boxed values. + */ + _Bool array_is_boxed; + + /* function for importing arrays. strings are specially handled (as they + are copied deeply), whereas other arrays are just copied shallowly + (but copied nevertheles) */ + array_elementwise_import_fn array_elementwise_import; + + /* a function for releasing an element when put in an array; integers + * don't need to be released, while ie. strings and arrays do. */ + array_element_release_fn array_element_release; + + /* a function for releasing a value. everything needs to be released. */ + value_release_fn value_release; +} data_meta_type_t; + +struct data_printer_context; + +typedef struct data_printer { + void (*print_data)(struct data_printer_context *printer_ctx, data_t *data, FILE *file); + void (*print_array)(struct data_printer_context *printer_ctx, data_array_t *data, FILE *file); + void (*print_string)(struct data_printer_context *printer_ctx, const char *data, FILE *file); + void (*print_double)(struct data_printer_context *printer_ctx, double data, FILE *file); + void (*print_int)(struct data_printer_context *printer_ctx, int data, FILE *file); +} data_printer_t; + +typedef struct data_printer_context { + data_printer_t *printer; +} data_printer_context_t; + +static data_meta_type_t dmt[DATA_COUNT] = { + // DATA_DATA + { array_element_size : sizeof(data_t*), + array_is_boxed : true, + array_elementwise_import : NULL, + array_element_release : (array_element_release_fn) data_free, + value_release : (value_release_fn) data_free }, + + // DATA_INT + { array_element_size : sizeof(int), + array_is_boxed : false, + array_elementwise_import : NULL, + array_element_release : NULL, + value_release : (value_release_fn) free }, + + // DATA_DOUBLE + { array_element_size : sizeof(double), + array_is_boxed : false, + array_elementwise_import : NULL, + array_element_release : NULL, + value_release : (value_release_fn) free }, + + // DATA_STRING + { array_element_size : sizeof(char*), + array_is_boxed : true, + array_elementwise_import : (array_elementwise_import_fn) strdup, + array_element_release : (array_element_release_fn) free, + value_release : (value_release_fn) free }, + + // DATA_ARRAY + { array_element_size : sizeof(data_array_t*), + array_is_boxed : true, + array_elementwise_import : NULL, + array_element_release : (array_element_release_fn) data_array_free , + value_release : (value_release_fn) data_array_free }, +}; + +static void print_json_data(data_printer_context_t *printer_ctx, data_t *data, FILE *file); +static void print_json_array(data_printer_context_t *printer_ctx, data_array_t *data, FILE *file); +static void print_json_string(data_printer_context_t *printer_ctx, const char *data, FILE *file); +static void print_json_double(data_printer_context_t *printer_ctx, double data, FILE *file); +static void print_json_int(data_printer_context_t *printer_ctx, int data, FILE *file); + +static data_printer_t json_printer = { + print_data : print_json_data, + print_array : print_json_array, + print_string : print_json_string, + print_double : print_json_double, + print_int : print_json_int +}; + +static _Bool import_values(void* dst, void* src, int num_values, data_type_t type) { + int element_size = dmt[type].array_element_size; + array_elementwise_import_fn import = dmt[type].array_elementwise_import; + if (import) { + for (int i = 0; i < num_values; ++i) { + void *copy = import(*(void**)(src + element_size * i)); + if (!copy) { + --i; + while (i >= 0) { + free(*(void**)(dst + element_size * i)); + --i; + } + return false; + } else { + *((char**) dst + i) = copy; + } + + } + } else { + memcpy(dst, src, element_size * num_values); + } + return true; // error is returned early +} + +data_array_t *data_array(int num_values, data_type_t type, void *values) { + data_array_t *array = calloc(1, sizeof(data_array_t)); + if (array) { + int element_size = dmt[type].array_element_size; + array->values = calloc(num_values, element_size); + if (!array->values) + goto alloc_error; + if (!import_values(array->values, values, num_values, type)) + goto alloc_error; + + array->num_values = num_values; + array->type = type; + } + return array; + + alloc_error: + if (array) + free(array->values); + free(array); + return NULL; +} + +data_t *data_make(const char *key, const char *pretty_key, data_type_t type, ...) { + va_list ap; + va_start(ap, type); + + data_t *first = NULL; + data_t *prev = NULL; + do { + data_t *current; + void *value = NULL; + + switch (type) { + case DATA_COUNT : { + assert(0); + } break; + case DATA_DATA : { + value = va_arg(ap, data_t*); + } break; + case DATA_INT : { + value = malloc(sizeof(int)); + if (value) + *(int*) value = va_arg(ap, int); + } break; + case DATA_DOUBLE : { + value = malloc(sizeof(double)); + if (value) + *(double*) value = va_arg(ap, double); + } break; + case DATA_STRING : { + value = strdup(va_arg(ap, char*)); + } break; + case DATA_ARRAY : { + value = va_arg(ap, data_t*); + } break; + } + + // also some null arguments are mapped to an alloc error; + // that's ok, because they originate (typically..) from + // an alloc error anyway + if (!value) + goto alloc_error; + + current = calloc(1, sizeof(*current)); + if (!current) + goto alloc_error; + if (prev) + prev->next = current; + + current->key = strdup(key); + if (!current->key) + goto alloc_error; + current->pretty_key = strdup(pretty_key ? pretty_key : key); + if (!current->pretty_key) + goto alloc_error; + current->type = type; + current->value = value; + current->next = NULL; + + prev = current; + if (!first) + first = current; + + key = va_arg(ap, const char*); + if (key) { + pretty_key = va_arg(ap, const char*); + type = va_arg(ap, data_type_t); + } + } while (key); + va_end(ap); + + return first; + + alloc_error: + data_free(first); + va_end(ap); + return NULL; +} + +void data_array_free(data_array_t *array) { + array_element_release_fn release = dmt[array->type].array_element_release; + if (release) { + int element_size = dmt[array->type].array_element_size; + for (int i = 0; i < array->num_values; ++i) + release(*(void**)(array->values + element_size * i)); + } + free(array->values); + free(array); +} + +void data_free(data_t *data) { + while (data) { + data_t *prev_data = data; + if (dmt[data->type].value_release) + dmt[data->type].value_release(data->value); + free(data->pretty_key); + free(data->key); + data = data->next; + free(prev_data); + } +} + +void data_print(data_t* data, FILE *file, data_printer_context_t *printer_ctx) +{ + printer_ctx->printer->print_data(printer_ctx, data, file); +} + +void data_print_json(data_t* data, FILE* file) { + data_printer_context_t ctx = { + printer : &json_printer + }; + ctx.printer->print_data(&ctx, data, file); + fputc('\n', file); +} + +/* JSON printer */ +static void print_json_array(data_printer_context_t *printer_ctx, data_array_t *array, FILE *file); + +static void print_value(data_printer_context_t *printer_ctx, FILE *file, data_type_t type, void *value) { + switch (type) { + case DATA_COUNT : { + assert(0); + } break; + case DATA_DATA : { + printer_ctx->printer->print_data(printer_ctx, value, file); + } break; + case DATA_INT : { + printer_ctx->printer->print_int(printer_ctx, *(int*) value, file); + } break; + case DATA_DOUBLE : { + printer_ctx->printer->print_double(printer_ctx, *(double*) value, file); + } break; + case DATA_STRING : { + printer_ctx->printer->print_string(printer_ctx, value, file); + } break; + case DATA_ARRAY : { + printer_ctx->printer->print_array(printer_ctx, value, file); + } break; + } +} + +static void print_json_array(data_printer_context_t *printer_ctx, data_array_t *array, FILE *file) { + int element_size = dmt[array->type].array_element_size; + char buffer[element_size]; + fprintf(file, "["); + for (int c = 0; c < array->num_values; ++c) { + if (c) + fprintf(file, ", "); + if (!dmt[array->type].array_is_boxed) { + memcpy(buffer, (void**)(array->values + element_size * c), element_size); + print_value(printer_ctx, file, array->type, buffer); + } else { + print_value(printer_ctx, file, array->type, *(void**)(array->values + element_size * c)); + } + } + fprintf(file, "]"); +} + +static void print_json_data(data_printer_context_t *printer_ctx, data_t *data, FILE *file) +{ + _Bool prev = false; + fputc('{', file); + while (data) { + if (prev) + fprintf(file, ", "); + print_json_string(printer_ctx, data->key, file); + fprintf(file, " : "); + print_value(printer_ctx, file, data->type, data->value); + prev = true; + data = data->next; + } + fputc('}', file); +} + +static void print_json_string(data_printer_context_t *printer_ctx, const char *str, FILE *file) { + fprintf(file, "\""); + while (*str) { + if (*str == '"') + fputc('\\', file); + fputc(*str, file); + ++str; + } + fprintf(file, "\""); +} + +static void print_json_double(data_printer_context_t *printer_ctx, double data, FILE *file) +{ + fprintf(file, "%f", data); +} + +static void print_json_int(data_printer_context_t *printer_ctx, int data, FILE *file) +{ + fprintf(file, "%d", data); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 00000000..1802e39f --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(data-test data-test.c) + +target_link_libraries(data-test data) diff --git a/tests/data-test.c b/tests/data-test.c new file mode 100644 index 00000000..8088a7f2 --- /dev/null +++ b/tests/data-test.c @@ -0,0 +1,40 @@ +/* + * A general structure for extracting hierarchical data from the devices; + * typically key-value pairs, but allows for more rich data as well + * + * Copyright (C) 2015 by Erkki Seppälä + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include "data.h" + +int main() +{ + data_t *data = data_make("label" , "", DATA_STRING, "1.2.3", + "house_code" , "House Code", DATA_INT, 42, + "temp" , "Temperature", DATA_DOUBLE, 99.9, + "array" , "Array", DATA_ARRAY, data_array(2, DATA_STRING, (char*[2]){"hello", "world"}), + "array2" , "Array 2", DATA_ARRAY, data_array(2, DATA_INT, (int[2]){4, 2}), + "array3" , "Array 3", DATA_ARRAY, data_array(2, DATA_ARRAY, (data_array_t*[2]){ + data_array(2, DATA_INT, (int[2]){4, 2}), + data_array(2, DATA_INT, (int[2]){5, 5}) }), + "data" , "Data", DATA_DATA, data_make("Hello", "hello", DATA_STRING, "world", NULL), + NULL); + data_print_json(data, stdout); + fprintf(stdout, "\n"); + data_free(data); +}