diff --git a/sys/include/phydat.h b/sys/include/phydat.h index caa594cca1..a85968bfbc 100644 --- a/sys/include/phydat.h +++ b/sys/include/phydat.h @@ -35,6 +35,7 @@ #ifndef PHYDAT_H #define PHYDAT_H +#include #include #include "kernel_defines.h" @@ -179,6 +180,23 @@ void phydat_dump(phydat_t *data, uint8_t dim); */ const char *phydat_unit_to_str(uint8_t unit); +/** + * @brief Return a string representation for every unit, including + * non-physical units like 'none' or 'time' + * + * This function is useful when converting phydat_t structures to non-binary + * representations like JSON or XML. + * + * In practice, this function extends phydat_unit_to_str() with additional + * identifiers for non physical units. + * + * @param[in] unit unit to convert + * + * @return string representation of given unit + * @return empty string ("") if unit was not recognized + */ +const char *phydat_unit_to_str_verbose(uint8_t unit); + /** * @brief Convert the given scale factor to an SI prefix * @@ -222,6 +240,51 @@ char phydat_prefix_from_scale(int8_t scale); */ void phydat_fit(phydat_t *dat, const int32_t *values, unsigned int dim); +/** + * @brief Convert the given phydat_t structure into a JSON string + * + * The output string written to @p buf will be `\0` terminated. You must make + * sure, that the given @p buf is large enough to hold the resulting string. You + * can call the function with `@p buf := NULL` to simply calculate the size of + * the JSON string without writing anything. + * + * The formatted JSON string will have the following format: + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ {.json} + * // case (dim == 1): + * { + * "d": 21.45, + * "u": "°C" + * } + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ {.json} + * // case (dim > 1), dim := 3 in this case: + * { + * "d": [1.02, 0.23, -0.81], + * "u": "g" + * } + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * + * The data will be encoded as fixed point number based on the given scale + * factor. + * + * For encoding the unit, this function uses the extended + * phydat_unit_to_str_verbose() function to also print units for non-SI types, + * e.g. it will produce `..."u":"date"}` for @ref UNIT_DATE or `..."u":"none"}` + * for @ref UNIT_NONE. + * + * @param[in] data data to encode + * @param[in] dim dimensions used in @p data, MUST be > 0 and < PHYDAT_DIM + * @param[out] buf target buffer for the JSON string, or NULL + * + * @pre @p dim > 0 + * @pre @p dim < PHYDAT_DIM + * + * @return number of bytes (potentially) written to @p buf, including `\0` + * terminator + */ +size_t phydat_to_json(const phydat_t *data, size_t dim, char *buf); + #ifdef __cplusplus } #endif diff --git a/sys/phydat/phydat_json.c b/sys/phydat/phydat_json.c new file mode 100644 index 0000000000..d2c3675772 --- /dev/null +++ b/sys/phydat/phydat_json.c @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2017 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup sys_phydat + * @{ + * + * @file + * @brief Convert phydat_t structs to human readable JSON strings + * + * @author Hauke Petersen + * + * @} + */ + +#include + +#include "fmt.h" +#include "assert.h" +#include "phydat.h" + +#define STATIC_LEN (14U) + +/** + * @note @p buf must be at least 5 bytes of size + */ +static size_t _bool_to_str(int16_t val, char *buf) +{ + if (val) { + memcpy(buf, "true", 4); + return 4; + } + else { + memcpy(buf, "false", 5); + return 5; + } +} + +size_t phydat_to_json(const phydat_t *data, size_t dim, char *buf) +{ + assert((dim > 0) && (dim < PHYDAT_DIM)); + + size_t pos = 0; + + if (buf == NULL) { + pos = STATIC_LEN; + if (dim > 1) { + pos += (2 + (dim - 1)); /* array parens + separating commas */ + } + for (size_t i = 0; i < dim; i++) { + if (data->unit != UNIT_BOOL) { + pos += fmt_s16_dfp(NULL, data->val[i], (int)data->scale); + } + else { + pos += (data->val[i]) ? 4 : 5; /* true: 4, false: 5 */ + } + } + pos += strlen(phydat_unit_to_str_verbose(data->unit)); + } + else { + memcpy(buf, "{\"d\":", 5); + pos += 5; + /* write data */ + if (dim > 1) { + buf[pos++] = '['; + } + for (size_t i = 0; i < dim; i++) { + if (data->unit != UNIT_BOOL) { + pos += fmt_s16_dfp(&buf[pos], data->val[i], (int)data->scale); + } + else { + pos += _bool_to_str(data->val[i], &buf[pos]); + } + buf[pos++] = ','; + } + /* override last comma if needed */ + if (dim > 1) { + buf[pos - 1] = ']'; + buf[pos++] = ','; + } + /* add unit */ + memcpy(&buf[pos], "\"u\":\"", 5); + pos += 5; + const char *u = phydat_unit_to_str_verbose(data->unit); + strcpy(&buf[pos], u); + pos += strlen(u); + /* terminate the JSON string */ + memcpy(&buf[pos], "\"}", 2); + pos += 2; + buf[pos++] = '\0'; + } + + return pos; +} diff --git a/sys/phydat/phydat_str.c b/sys/phydat/phydat_str.c index 0f2243e5f6..bbdea52bad 100644 --- a/sys/phydat/phydat_str.c +++ b/sys/phydat/phydat_str.c @@ -11,7 +11,7 @@ * @{ * * @file - * @brief Generic sensor/actuator data handling + * @brief String helper functions for formatting and dumping phydat data * * @author Hauke Petersen * @@ -133,6 +133,19 @@ const char *phydat_unit_to_str(uint8_t unit) } } +const char *phydat_unit_to_str_verbose(uint8_t unit) +{ + switch (unit) { + case UNIT_UNDEF: return "undefined"; + case UNIT_NONE: /* fall through */ + case UNIT_BOOL: + return "none"; + case UNIT_TIME: return "time"; + case UNIT_DATE: return "date"; + default: return phydat_unit_to_str(unit); + } +} + char phydat_prefix_from_scale(int8_t scale) { switch (scale) { diff --git a/tests/unittests/tests-phydat/tests-phydat.c b/tests/unittests/tests-phydat/tests-phydat.c index 54324da40f..ef7de2410a 100644 --- a/tests/unittests/tests-phydat/tests-phydat.c +++ b/tests/unittests/tests-phydat/tests-phydat.c @@ -1,12 +1,27 @@ /* * Copyright (C) 2018 Eistec AB * 2018 Otto-von-Guericke-Universität Magdeburg + * 2017 Freie Universität Berlin * * This file is subject to the terms and conditions of the GNU Lesser * General Public License v2.1. See the file LICENSE in the top level * directory for more details. */ +/** + * @{ + * + * @file + * @brief Unittests for the phydat module + * + * @author Joakim Nohlgård + * @author Hauke Petersen + * + * @} + */ + +#include + #include "embUnit.h" #include "tests-phydat.h" @@ -20,6 +35,65 @@ #define PHYDAT_FIT_TRADE_PRECISION_FOR_ROM 1 #endif +#if (PHYDAT_DIM != 3) +#error "PHYDAT unittests are only applicable if PHYDAT_DIM is 3" +#endif + +#define BUFSIZE (128U) + +static char test[BUFSIZE]; + +typedef struct { + int dim; + phydat_t dat; + const char *json; +} tdat_t; + +/* define test data */ +static tdat_t data[] = { + { + .dim = 1, + .dat = { { 2345, 0, 0 }, UNIT_TEMP_C, -2 }, + .json = "{\"d\":23.45,\"u\":\"°C\"}", + }, + { + .dim = 3, + .dat = { { 1032, 10, -509 }, UNIT_G, -3 }, + .json = "{\"d\":[1.032,0.010,-0.509],\"u\":\"g\"}", + }, + { + .dim = 3, + .dat = { { 1200, 38, 98 }, UNIT_M, -3 }, + .json = "{\"d\":[1.200,0.038,0.098],\"u\":\"m\"}", + }, + { + .dim = 2, + .dat = { { 19, 23, 0 }, UNIT_NONE, 0 }, + .json = "{\"d\":[19,23],\"u\":\"none\"}", + }, + { + .dim = 1, + .dat = { { 1, 0, 0 }, UNIT_BOOL, 0 }, + .json = "{\"d\":true,\"u\":\"none\"}", + }, + { + .dim = 3, + .dat = { { 1, 0, 1 }, UNIT_BOOL, 0 }, + .json = "{\"d\":[true,false,true],\"u\":\"none\"}", + }, + { + .dim = 3, + .dat = { { 1, 1, 1970 }, UNIT_DATE, 0 }, + .json = "{\"d\":[1,1,1970],\"u\":\"date\"}", + }, + { + .dim = 3, + .dat = { { 23, 59, 14}, UNIT_TIME, 0 }, + .json = "\"d\":[23,59,14],\"u\":\"time\"}", + } + +}; + static void test_phydat_fit(void) { /* Input values for each test: */ @@ -114,10 +188,94 @@ static void test_phydat_fit(void) } } +static void test_unitstr__success(void) +{ + /* test the verbose cases */ + TEST_ASSERT_EQUAL_STRING("undefined", phydat_unit_to_str_verbose(UNIT_UNDEF)); + TEST_ASSERT_EQUAL_STRING("none", phydat_unit_to_str_verbose(UNIT_NONE)); + TEST_ASSERT_EQUAL_STRING("none", phydat_unit_to_str_verbose(UNIT_BOOL)); + TEST_ASSERT_EQUAL_STRING("time", phydat_unit_to_str_verbose(UNIT_TIME)); + TEST_ASSERT_EQUAL_STRING("date", phydat_unit_to_str_verbose(UNIT_DATE)); + + TEST_ASSERT_EQUAL_STRING("°C", phydat_unit_to_str_verbose(UNIT_TEMP_C)); + TEST_ASSERT_EQUAL_STRING("°F", phydat_unit_to_str_verbose(UNIT_TEMP_F)); + TEST_ASSERT_EQUAL_STRING("K", phydat_unit_to_str_verbose(UNIT_TEMP_K)); + TEST_ASSERT_EQUAL_STRING("lx", phydat_unit_to_str_verbose(UNIT_LUX)); + TEST_ASSERT_EQUAL_STRING("m", phydat_unit_to_str_verbose(UNIT_M)); + TEST_ASSERT_EQUAL_STRING("m^2", phydat_unit_to_str_verbose(UNIT_M2)); + TEST_ASSERT_EQUAL_STRING("m^3", phydat_unit_to_str_verbose(UNIT_M3)); + TEST_ASSERT_EQUAL_STRING("g", phydat_unit_to_str_verbose(UNIT_G)); + TEST_ASSERT_EQUAL_STRING("dps", phydat_unit_to_str_verbose(UNIT_DPS)); + TEST_ASSERT_EQUAL_STRING("G", phydat_unit_to_str_verbose(UNIT_GR)); + TEST_ASSERT_EQUAL_STRING("A", phydat_unit_to_str_verbose(UNIT_A)); + TEST_ASSERT_EQUAL_STRING("V", phydat_unit_to_str_verbose(UNIT_V)); + TEST_ASSERT_EQUAL_STRING("W", phydat_unit_to_str_verbose(UNIT_W)); + TEST_ASSERT_EQUAL_STRING("dBm", phydat_unit_to_str_verbose(UNIT_DBM)); + TEST_ASSERT_EQUAL_STRING("Gs", phydat_unit_to_str_verbose(UNIT_GS)); + TEST_ASSERT_EQUAL_STRING("Bar", phydat_unit_to_str_verbose(UNIT_BAR)); + TEST_ASSERT_EQUAL_STRING("Pa", phydat_unit_to_str_verbose(UNIT_PA)); + TEST_ASSERT_EQUAL_STRING("permille", phydat_unit_to_str_verbose(UNIT_PERMILL)); + TEST_ASSERT_EQUAL_STRING("ppm", phydat_unit_to_str_verbose(UNIT_PPM)); + TEST_ASSERT_EQUAL_STRING("ppb", phydat_unit_to_str_verbose(UNIT_PPB)); + TEST_ASSERT_EQUAL_STRING("cd", phydat_unit_to_str_verbose(UNIT_CD)); + TEST_ASSERT_EQUAL_STRING("%", phydat_unit_to_str_verbose(UNIT_PERCENT)); + TEST_ASSERT_EQUAL_STRING("cts", phydat_unit_to_str_verbose(UNIT_CTS)); + TEST_ASSERT_EQUAL_STRING("C", phydat_unit_to_str_verbose(UNIT_COULOMB)); + TEST_ASSERT_EQUAL_STRING("g/m^3", phydat_unit_to_str_verbose(UNIT_GPM3)); + TEST_ASSERT_EQUAL_STRING("F", phydat_unit_to_str_verbose(UNIT_F)); + TEST_ASSERT_EQUAL_STRING("pH", phydat_unit_to_str_verbose(UNIT_PH)); + TEST_ASSERT_EQUAL_STRING("#/m^3", phydat_unit_to_str_verbose(UNIT_CPM3)); + TEST_ASSERT_EQUAL_STRING("ohm", phydat_unit_to_str_verbose(UNIT_OHM)); + +} + +static void test_json__success(void) +{ + size_t len; + + len = phydat_to_json(&data[0].dat, data[0].dim, NULL); + TEST_ASSERT_EQUAL_INT((strlen(data[0].json) + 1), len); + len = phydat_to_json(&data[0].dat, data[0].dim, test); + TEST_ASSERT_EQUAL_INT(strlen(data[0].json), strlen(test)); + TEST_ASSERT_EQUAL_STRING(data[0].json, (const char *)test); + + len = phydat_to_json(&data[1].dat, data[1].dim, NULL); + TEST_ASSERT_EQUAL_INT((strlen(data[1].json) + 1), len); + len = phydat_to_json(&data[1].dat, data[1].dim, test); + TEST_ASSERT_EQUAL_INT(strlen(data[1].json), strlen(test)); + TEST_ASSERT_EQUAL_STRING(data[1].json, (const char *)test); + + len = phydat_to_json(&data[2].dat, data[2].dim, NULL); + TEST_ASSERT_EQUAL_INT((strlen(data[2].json) + 1), len); + len = phydat_to_json(&data[2].dat, data[2].dim, test); + TEST_ASSERT_EQUAL_INT(strlen(data[2].json), strlen(test)); + TEST_ASSERT_EQUAL_STRING(data[2].json, (const char *)test); + + len = phydat_to_json(&data[3].dat, data[3].dim, NULL); + TEST_ASSERT_EQUAL_INT((strlen(data[3].json) + 1), len); + len = phydat_to_json(&data[3].dat, data[3].dim, test); + TEST_ASSERT_EQUAL_INT(strlen(data[3].json), strlen(test)); + TEST_ASSERT_EQUAL_STRING(data[3].json, (const char *)test); + + len = phydat_to_json(&data[4].dat, data[4].dim, NULL); + TEST_ASSERT_EQUAL_INT((strlen(data[4].json) + 1), len); + len = phydat_to_json(&data[4].dat, data[4].dim, test); + TEST_ASSERT_EQUAL_INT(strlen(data[4].json), strlen(test)); + TEST_ASSERT_EQUAL_STRING(data[4].json, (const char *)test); + + len = phydat_to_json(&data[5].dat, data[5].dim, NULL); + TEST_ASSERT_EQUAL_INT((strlen(data[5].json) + 1), len); + len = phydat_to_json(&data[5].dat, data[5].dim, test); + TEST_ASSERT_EQUAL_INT(strlen(data[5].json), strlen(test)); + TEST_ASSERT_EQUAL_STRING(data[5].json, (const char *)test); +} + Test *tests_phydat_tests(void) { EMB_UNIT_TESTFIXTURES(fixtures) { new_TestFixture(test_phydat_fit), + new_TestFixture(test_unitstr__success), + new_TestFixture(test_json__success), }; EMB_UNIT_TESTCALLER(phydat_tests, NULL, NULL, fixtures); diff --git a/tests/unittests/tests-phydat/tests-phydat.h b/tests/unittests/tests-phydat/tests-phydat.h index 1697ecba83..6b1682d173 100644 --- a/tests/unittests/tests-phydat/tests-phydat.h +++ b/tests/unittests/tests-phydat/tests-phydat.h @@ -1,5 +1,6 @@ /* * Copyright (C) 2018 Eistec AB + * Copyright (C) 2017 Freie Universität Berlin * * This file is subject to the terms and conditions of the GNU Lesser * General Public License v2.1. See the file LICENSE in the top level @@ -14,18 +15,21 @@ * @brief Unittests for the phydat module * * @author Joakim Nohlgård + * @author Hauke Petersen */ + #ifndef TESTS_PHYDAT_H #define TESTS_PHYDAT_H -#include "embUnit/embUnit.h" + +#include "embUnit.h" #ifdef __cplusplus extern "C" { #endif /** -* @brief The entry point of this test suite. -*/ + * @brief The entry point of this test suite. + */ void tests_phydat(void); #ifdef __cplusplus