diff --git a/platformio.ini b/platformio.ini index 2dd0f64..6525dc6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -36,6 +36,7 @@ build_flags = -D TS_64BIT_TYPES_SUPPORT=1 -D TS_DECFRAC_TYPE_SUPPORT=1 -D TS_BYTE_STRING_TYPE_SUPPORT=1 + -D TS_NESTED_JSON=1 # include src directory (otherwise unit-tests will only include lib directory) test_build_project_src = true diff --git a/src/thingset.c b/src/thingset.c index 45baac1..b5e450a 100644 --- a/src/thingset.c +++ b/src/thingset.c @@ -65,11 +65,17 @@ int ts_process(struct ts_context *ts, const uint8_t *request, size_t request_len } } -void ts_set_authentication(struct ts_context *ts, uint16_t flags) +void ts_set_authentication(struct ts_context *ts, uint8_t flags) { ts->_auth_flags = flags; } +void ts_set_update_callback(struct ts_context *ts, const uint16_t subsets, void (*update_cb)(void)) +{ + ts->_update_subsets = subsets; + ts->update_cb = update_cb; +} + struct ts_data_object *ts_get_object_by_name(struct ts_context *ts, const char *name, size_t len, int32_t parent) { diff --git a/src/thingset.h b/src/thingset.h index b59b154..3601b89 100644 --- a/src/thingset.h +++ b/src/thingset.h @@ -341,15 +341,15 @@ static inline void *ts_array_to_void(struct ts_array_info *ptr) { return (void * #define TS_ROLE_EXP (1U << 1) // expert user #define TS_ROLE_MKR (1U << 2) // maker -#define TS_READ_MASK 0x00FF // read flags stored in 4 least-significant bits -#define TS_WRITE_MASK 0xFF00 // write flags stored in 4 most-significant bits +#define TS_READ_MASK 0x0F // read flags stored in 4 least-significant bits +#define TS_WRITE_MASK 0xF0 // write flags stored in 4 most-significant bits -#define TS_USR_MASK (TS_ROLE_USR << 8 | TS_ROLE_USR) -#define TS_EXP_MASK (TS_ROLE_EXP << 8 | TS_ROLE_EXP) -#define TS_MKR_MASK (TS_ROLE_MKR << 8 | TS_ROLE_MKR) +#define TS_USR_MASK (TS_ROLE_USR << 4 | TS_ROLE_USR) +#define TS_EXP_MASK (TS_ROLE_EXP << 4 | TS_ROLE_EXP) +#define TS_MKR_MASK (TS_ROLE_MKR << 4 | TS_ROLE_MKR) #define TS_READ(roles) ((roles) & TS_READ_MASK) -#define TS_WRITE(roles) (((roles) << 8) & TS_WRITE_MASK) +#define TS_WRITE(roles) (((roles) << 4) & TS_WRITE_MASK) #define TS_READ_WRITE(roles) (TS_READ(roles) | TS_WRITE(roles)) #define TS_USR_R TS_READ(TS_ROLE_USR) @@ -401,24 +401,30 @@ struct ts_data_object { /** * One of TS_TYPE_INT32, _FLOAT, ... */ - const uint8_t type; + const uint32_t type : 4; /** - * Exponent (10^exponent = factor to convert to SI unit) for decimal fraction type, - * decimal digits to use for printing of floats in JSON strings or - * length of string buffer for string type + * Variable storing different detail information depending on th data type + * + * - FLOAT32: Decimal digits (precision) to use for printing in JSON strings. + * + * - DECFRAC: Exponent (10^exponent = factor to convert to internal unit). Example: If + * a voltage measurement is internally stored as an integer in mV, use exponent -3 to + * convert to the SI base unit V as exposed via ThingSet. + * + * - STRING or BYTES: Size of the internal buffer in bytes. */ - const int16_t detail; + const int32_t detail : 12; /** * Flags to define read/write access */ - const uint16_t access; + const uint32_t access : 8; /** * Flags to assign data item to different data item subsets (e.g. for publication messages) */ - uint16_t subsets; + uint32_t subsets : 8; }; @@ -479,7 +485,18 @@ struct ts_context { /** * Stores current authentication status (authentication as "normal" user as default) */ - uint16_t _auth_flags; + uint8_t _auth_flags; + + /** + * Stores current authentication status (authentication as "normal" user as default) + */ + uint8_t _update_subsets; + + /** + * Callback to be called from patch function if a value belonging to _update_subsets + * was changed + */ + void (*update_cb)(void); }; /** @@ -529,7 +546,16 @@ void ts_dump_json(struct ts_context *ts, ts_object_id_t obj_id, int level); * @param ts Pointer to ThingSet context. * @param flags Flags to define authentication level (1 = access allowed) */ -void ts_set_authentication(struct ts_context *ts, uint16_t flags); +void ts_set_authentication(struct ts_context *ts, uint8_t flags); + +/** + * Configures a callback for notification if data belonging to specified subset(s) was updated. + * + * @param ts Pointer to ThingSet context. + * @param subsets Flags to select which subset(s) of data items should be considered + * @param update_cb Callback to be called after an update. + */ +void ts_set_update_callback(struct ts_context *ts, const uint16_t subsets, void (*update_cb)(void)); /** * Retrieve data in JSON format for given subset(s). @@ -676,7 +702,7 @@ int ts_bin_pub_can(struct ts_context *ts, int *start_pos, uint16_t subset, uint8 * * @returns ThingSet status code */ -int ts_bin_import(struct ts_context *ts, uint8_t *data, size_t len, uint16_t auth_flags, +int ts_bin_import(struct ts_context *ts, uint8_t *data, size_t len, uint8_t auth_flags, uint16_t subsets); /** @@ -760,11 +786,16 @@ class ThingSet ts_dump_json(&ts, obj_id, level); }; - inline void set_authentication(uint16_t flags) + inline void set_authentication(uint8_t flags) { ts_set_authentication(&ts, flags); }; + inline void set_update_callback(const uint16_t subsets, void (*update_cb)(void)) + { + ts_set_update_callback(&ts, subsets, update_cb); + }; + inline int txt_export(char *buf, size_t size, const uint16_t subsets) { return ts_txt_export(&ts, buf, size, subsets); @@ -790,7 +821,7 @@ class ThingSet return ts_bin_export(&ts, buf, size, subsets); }; - inline int bin_import(uint8_t *buf, size_t size, uint16_t auth_flags, const uint16_t subsets) + inline int bin_import(uint8_t *buf, size_t size, uint8_t auth_flags, const uint16_t subsets) { return ts_bin_import(&ts, buf, size, auth_flags, subsets); }; @@ -880,7 +911,7 @@ class ThingSet * * @returns ThingSet status code */ - inline int bin_sub(uint8_t *cbor_data, size_t len, uint16_t auth_flags, uint16_t subsets) + inline int bin_sub(uint8_t *cbor_data, size_t len, uint8_t auth_flags, uint16_t subsets) __attribute__((deprecated)) { return ts_bin_import(&ts, cbor_data + 1, len - 1, auth_flags, subsets); diff --git a/src/thingset_bin.c b/src/thingset_bin.c index a7aca59..c47d5a1 100644 --- a/src/thingset_bin.c +++ b/src/thingset_bin.c @@ -345,7 +345,7 @@ int ts_bin_fetch(struct ts_context *ts, const struct ts_data_object *parent, uin } } -int ts_bin_import(struct ts_context *ts, uint8_t *data, size_t len, uint16_t auth_flags, +int ts_bin_import(struct ts_context *ts, uint8_t *data, size_t len, uint8_t auth_flags, uint16_t subsets) { uint8_t resp_tmp[1] = {}; // only one character as response expected @@ -358,10 +358,11 @@ int ts_bin_import(struct ts_context *ts, uint8_t *data, size_t len, uint16_t aut } int ts_bin_patch(struct ts_context *ts, const struct ts_data_object *parent, - unsigned int pos_payload, uint16_t auth_flags, uint16_t subsets) + unsigned int pos_payload, uint8_t auth_flags, uint16_t subsets) { unsigned int pos_req = pos_payload; uint16_t num_elements, element = 0; + bool updated = false; if ((ts->req[pos_req] & CBOR_TYPE_MASK) != CBOR_MAP) { return ts_bin_response(ts, TS_STATUS_BAD_REQUEST); @@ -403,6 +404,9 @@ int ts_bin_patch(struct ts_context *ts, const struct ts_data_object *parent, else { // actually deserialize the data and update object num_bytes = cbor_deserialize_data_obj(&ts->req[pos_req], object); + if (ts->_update_subsets & object->subsets) { + updated = true; + } } } else { @@ -425,6 +429,9 @@ int ts_bin_patch(struct ts_context *ts, const struct ts_data_object *parent, } if (element == num_elements) { + if (updated && ts->update_cb != NULL) { + ts->update_cb(); + } return ts_bin_response(ts, TS_STATUS_CHANGED); } else { return ts_bin_response(ts, TS_STATUS_BAD_REQUEST); diff --git a/src/thingset_priv.h b/src/thingset_priv.h index 562036d..c6bb9d8 100644 --- a/src/thingset_priv.h +++ b/src/thingset_priv.h @@ -114,7 +114,7 @@ int ts_txt_patch(struct ts_context *ts, const struct ts_data_object *parent); * @param subsets Bitset to specifiy data item subsets to be considered, 0 to ignore */ int ts_bin_patch(struct ts_context *ts, const struct ts_data_object *parent, - unsigned int pos_payload, uint16_t auth_flags, uint16_t subsets); + unsigned int pos_payload, uint8_t auth_flags, uint16_t subsets); /** * POST request to append data. diff --git a/src/thingset_txt.c b/src/thingset_txt.c index 1ebb920..d8dd819 100644 --- a/src/thingset_txt.c +++ b/src/thingset_txt.c @@ -163,7 +163,21 @@ int ts_json_serialize_value(struct ts_context *ts, char *buf, size_t size, pos = snprintf(&buf[pos], size - pos, "["); for (unsigned int i = 0; i < ts->num_objects; i++) { if (ts->data_objects[i].subsets & (uint16_t)object->detail) { +#if TS_NESTED_JSON + if (ts->data_objects[i].parent == 0) { + pos += snprintf(&buf[pos], size - pos, "\"%s\",", ts->data_objects[i].name); + } + else { + struct ts_data_object *parent_obj = + ts_get_object_by_id(ts, ts->data_objects[i].parent); + if (parent_obj != NULL) { + pos += snprintf(&buf[pos], size - pos, "\"%s/%s\",", + parent_obj->name, ts->data_objects[i].name); + } + } +#else pos += snprintf(&buf[pos], size - pos, "\"%s\",", ts->data_objects[i].name); +#endif } } if (pos > 1) { @@ -502,6 +516,7 @@ int ts_json_deserialize_value(struct ts_context *ts, char *buf, size_t len, jsmn int ts_txt_patch(struct ts_context *ts, const struct ts_data_object *parent) { int tok = 0; // current token + bool updated = false; // buffer for data object value (largest negative 64bit integer has 20 digits) char value_buf[21]; @@ -598,6 +613,14 @@ int ts_txt_patch(struct ts_context *ts, const struct ts_data_object *parent) tok += ts_json_deserialize_value(ts, &ts->json_str[ts->tokens[tok].start], value_len, ts->tokens[tok].type, object); + + if (ts->_update_subsets & object->subsets) { + updated = true; + } + } + + if (updated && ts->update_cb != NULL) { + ts->update_cb(); } return ts_txt_response(ts, TS_STATUS_CHANGED); @@ -683,10 +706,15 @@ int ts_txt_create(struct ts_context *ts, const struct ts_data_object *object) } else if (object->type == TS_T_SUBSET) { if (ts->tokens[0].type == JSMN_STRING) { - struct ts_data_object *del_object = ts_get_object_by_name(ts, ts->json_str + +#if TS_NESTED_JSON + struct ts_data_object *add_object = ts_get_object_by_path(ts, ts->json_str + + ts->tokens[0].start, ts->tokens[0].end - ts->tokens[0].start); +#else + struct ts_data_object *add_object = ts_get_object_by_name(ts, ts->json_str + ts->tokens[0].start, ts->tokens[0].end - ts->tokens[0].start, -1); - if (del_object != NULL) { - del_object->subsets |= (uint16_t)object->detail; +#endif + if (add_object != NULL) { + add_object->subsets |= (uint16_t)object->detail; return ts_txt_response(ts, TS_STATUS_CREATED); } return ts_txt_response(ts, TS_STATUS_NOT_FOUND); @@ -708,8 +736,13 @@ int ts_txt_delete(struct ts_context *ts, const struct ts_data_object *object) } else if (object->type == TS_T_SUBSET) { if (ts->tokens[0].type == JSMN_STRING) { +#if TS_NESTED_JSON + struct ts_data_object *del_object = ts_get_object_by_path(ts, ts->json_str + + ts->tokens[0].start, ts->tokens[0].end - ts->tokens[0].start); +#else struct ts_data_object *del_object = ts_get_object_by_name(ts, ts->json_str + ts->tokens[0].start, ts->tokens[0].end - ts->tokens[0].start, -1); +#endif if (del_object != NULL) { del_object->subsets &= ~((uint16_t)object->detail); return ts_txt_response(ts, TS_STATUS_DELETED); @@ -769,6 +802,49 @@ int ts_txt_exec(struct ts_context *ts, const struct ts_data_object *object) return ts_txt_response(ts, TS_STATUS_VALID); } +#if TS_NESTED_JSON + +/* currently only supporting nesting of depth 1 */ +int ts_txt_export(struct ts_context *ts, char *buf, size_t buf_size, uint16_t subsets) +{ + unsigned int len = 1; + buf[0] = '{'; + uint16_t prev_parent = 0; + unsigned int depth = 0; + + for (unsigned int i = 0; i < ts->num_objects; i++) { + if (ts->data_objects[i].subsets & subsets) { + const uint16_t parent_id = ts->data_objects[i].parent; + if (prev_parent != parent_id) { + if (prev_parent != 0) { + // close object of previous parent + buf[len-1] = '}'; // overwrite comma + buf[len++] = ','; + } + struct ts_data_object *parent = ts_get_object_by_id(ts, parent_id); + len += snprintf(&buf[len], buf_size - len, "\"%s\":{", parent->name); + prev_parent = parent_id; + depth = 1; + } + len += ts_json_serialize_name_value(ts, &buf[len], buf_size - len, + &ts->data_objects[i]); + } + if (len >= buf_size - 1 - depth) { + return 0; + } + } + + buf[len-1] = '}'; // overwrite comma + + if (depth == 1) { + buf[len++] = '}'; + } + + return len; +} + +#else + int ts_txt_export(struct ts_context *ts, char *buf, size_t buf_size, uint16_t subsets) { unsigned int len = 1; @@ -789,6 +865,8 @@ int ts_txt_export(struct ts_context *ts, char *buf, size_t buf_size, uint16_t su return len; } +#endif /* TS_NESTED_JSON */ + int ts_txt_statement(struct ts_context *ts, char *buf, size_t buf_size, struct ts_data_object *object) { diff --git a/src/ts_config.h b/src/ts_config.h index 7f81927..42f432c 100644 --- a/src/ts_config.h +++ b/src/ts_config.h @@ -75,4 +75,24 @@ #define TS_BYTE_STRING_TYPE_SUPPORT CONFIG_THINGSET_BYTE_STRING_TYPE_SUPPORT #endif +/* + * The ThingSet specification v0.5 introduces a different data layout compared to previous + * versions where the data is grouped by entities of the device (like battery, actuator) + * instead of the data type (e.g. measurement, configuration). The data type is described by + * a single character prefix in the item name. + * The new grouping allows to have same item names in different groups, so for unambigous + * description of the data the nested structure has to be maintained in statements. + * + * With nested JSON enabled, requesting names for IDs will also return the entire path (as + * a JSON pointer) instead of just the data item name. + * + * This option is introduced to maintain compatibility with legacy firmware and will be + * enabled by default in the future. + */ +#if !defined(TS_NESTED_JSON) && !defined(CONFIG_THINGSET_NESTED_JSON) +#define TS_NESTED_JSON 0 // default: no nested JSON for legacy code +#elif !defined(TS_NESTED_JSON) +#define TS_NESTED_JSON CONFIG_THINGSET_NESTED_JSON +#endif + #endif /* TS_CONFIG_H_ */ diff --git a/test/main.cpp b/test/main.cpp index 7935e22..0edfaaa 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -55,7 +55,7 @@ void tests_text_mode() RUN_TEST(test_txt_patch_readonly); RUN_TEST(test_txt_patch_wrong_path); RUN_TEST(test_txt_patch_unknown_object); - RUN_TEST(test_txt_conf_callback); + RUN_TEST(test_txt_group_callback); // POST request RUN_TEST(test_txt_exec); @@ -81,6 +81,9 @@ void tests_text_mode() // data export RUN_TEST(test_txt_export); + // update notification + RUN_TEST(test_txt_update_callback), + UNITY_END(); } @@ -128,6 +131,9 @@ void tests_binary_mode() RUN_TEST(test_bin_export); RUN_TEST(test_bin_import); + // update notification + RUN_TEST(test_bin_update_callback), + UNITY_END(); } diff --git a/test/test.h b/test/test.h index a8be964..e488709 100644 --- a/test/test.h +++ b/test/test.h @@ -36,6 +36,7 @@ extern "C" { #define SUBSET_REPORT (1U << 0) // report subset of data items for publication #define SUBSET_CAN (1U << 1) // data nodes used for CAN bus publication messages +#define SUBSET_NVM (1U << 2) // data that should be stored in EEPROM extern char manufacturer[]; extern bool pub_report_enable; @@ -69,13 +70,15 @@ extern struct ts_context ts; extern uint8_t req_buf[TS_REQ_BUFFER_LEN]; extern uint8_t resp_buf[TS_RESP_BUFFER_LEN]; -extern bool conf_callback_called; +extern bool group_callback_called; +extern bool update_callback_called; extern bool dummy_called_flag; extern struct ts_array_info pub_serial_array; /* Helper functions (see test_context.c) */ void dummy(void); -void conf_callback(void); +void group_callback(void); +void update_callback(void); void reset_function(void); void auth_function(void); int _hex2bin(uint8_t *bin, size_t bin_size, const char *hex); @@ -142,7 +145,7 @@ void test_txt_patch_array(void); void test_txt_patch_readonly(void); void test_txt_patch_wrong_path(void); void test_txt_patch_unknown_object(void); -void test_txt_conf_callback(void); +void test_txt_group_callback(void); void test_txt_exec(void); void test_txt_statement_subset(void); void test_txt_statement_group(void); @@ -157,6 +160,7 @@ void test_txt_auth_reset(void); void test_txt_wrong_command(void); void test_txt_get_endpoint(void); void test_txt_export(void); +void test_txt_update_callback(void); void test_bin_get_meas_ids_values(void); void test_bin_get_meas_names_values(void); @@ -180,6 +184,7 @@ void test_bin_deserialize_bytes(void); void test_bin_patch_fetch_bytes(void); void test_bin_export(void); void test_bin_import(void); +void test_bin_update_callback(void); #ifdef __cplusplus } /* extern "C" */ diff --git a/test/test_bin.c b/test/test_bin.c index 4ed420c..f10b3bb 100644 --- a/test/test_bin.c +++ b/test/test_bin.c @@ -206,7 +206,8 @@ void test_bin_patch_rounded_float(void) 0x05 }; const uint8_t resp_expected[] = { - TS_STATUS_CHANGED }; + TS_STATUS_CHANGED + }; TEST_ASSERT_BIN_REQ_EXP_BIN(req, sizeof(req), resp_expected, sizeof(resp_expected)); @@ -238,9 +239,8 @@ void test_bin_statement_group(void) const char resp_expected[] = "1F " "01 " // ID of "info" - "83 " // array with 3 elements + "82 " // array with 2 elements "6B 4C 69 62 72 65 20 53 6F 6C 61 72 " // "Libre Solar" - "1A 00 BC 61 4E " // int 12345678 "68 41 42 43 44 31 32 33 34 "; // "ABCD1234" int resp_len = ts_bin_statement_by_path(&ts, resp_buf, sizeof(resp_buf), "info"); @@ -428,7 +428,7 @@ void test_bin_export(void) { const char resp_expected[] = "A4 " // map with 4 elements - "18 1A 1A 00 BC 61 4E " // int 12345678 + "10 1A 00 BC 61 4E " // int 12345678 "18 71 FA 41 61 99 9a " // float 14.10 "18 72 FA 40 a4 28 f6 " // float 5.13 "18 73 16 "; // int 22 @@ -437,3 +437,29 @@ void test_bin_export(void) TEST_ASSERT_BIN_RESP(resp_buf, resp_len, resp_expected); } + +void test_bin_update_callback(void) +{ + const uint8_t req[] = { + TS_PATCH, + 0x18, ID_CONF, + 0xA1, + 0x18, 0x31, + 0x05 + }; + const uint8_t resp_expected[] = { + TS_STATUS_CHANGED + }; + + update_callback_called = false; + + // without callback + ts_set_update_callback(&ts, SUBSET_NVM, NULL); + TEST_ASSERT_BIN_REQ_EXP_BIN(req, sizeof(req), resp_expected, sizeof(resp_expected)); + TEST_ASSERT_EQUAL(false, update_callback_called); + + // with configured callback + ts_set_update_callback(&ts, SUBSET_NVM, update_callback); + TEST_ASSERT_BIN_REQ_EXP_BIN(req, sizeof(req), resp_expected, sizeof(resp_expected)); + TEST_ASSERT_EQUAL(true, update_callback_called); +} diff --git a/test/test_context.c b/test/test_context.c index 3efffce..40ad849 100644 --- a/test/test_context.c +++ b/test/test_context.c @@ -12,19 +12,25 @@ struct ts_context ts; uint8_t req_buf[TS_REQ_BUFFER_LEN]; uint8_t resp_buf[TS_RESP_BUFFER_LEN]; -bool conf_callback_called; +bool group_callback_called; +bool update_callback_called; bool dummy_called_flag; struct ts_array_info pub_serial_array; void dummy(void) { - dummy_called_flag = 1; + dummy_called_flag = true; } -void conf_callback(void) +void group_callback(void) { - conf_callback_called = 1; + group_callback_called = true; +} + +void update_callback(void) +{ + update_callback_called = true; } void reset_function() diff --git a/test/test_data.c b/test/test_data.c index 71ba011..e1c04fd 100644 --- a/test/test_data.c +++ b/test/test_data.c @@ -70,12 +70,11 @@ struct ts_array_info float32_array = {B, sizeof(B)/sizeof(float), 2, TS_T_FLOAT3 uint8_t bytes[300] = {}; struct ts_bytes_buffer bytes_buf = { bytes, 0 }; -void dummy(void); -void conf_callback(void); - - struct ts_data_object data_objects[] = { + TS_ITEM_UINT32(0x10, "t_s", ×tamp, + ID_ROOT, TS_ANY_RW, SUBSET_REPORT), + // DEVICE INFORMATION ///////////////////////////////////////////////////// TS_GROUP(ID_INFO, "info", TS_NO_CALLBACK, ID_ROOT), @@ -83,21 +82,18 @@ struct ts_data_object data_objects[] = { TS_ITEM_STRING(0x19, "Manufacturer", manufacturer, 0, ID_INFO, TS_ANY_R, 0), - TS_ITEM_UINT32(0x1A, "Timestamp_s", ×tamp, - ID_INFO, TS_ANY_RW, SUBSET_REPORT), - TS_ITEM_STRING(0x1B, "DeviceID", device_id, sizeof(device_id), - ID_INFO, TS_ANY_R | TS_MKR_W, 0), + ID_INFO, TS_ANY_R | TS_MKR_W, SUBSET_NVM), // CONFIGURATION ////////////////////////////////////////////////////////// - TS_GROUP(ID_CONF, "conf", &conf_callback, ID_ROOT), + TS_GROUP(ID_CONF, "conf", &group_callback, ID_ROOT), TS_ITEM_FLOAT(0x31, "BatCharging_V", &bat_charging_voltage, 2, - ID_CONF, TS_ANY_RW, 0), + ID_CONF, TS_ANY_RW, SUBSET_NVM), TS_ITEM_FLOAT(0x32, "LoadDisconnect_V", &load_disconnect_voltage, 2, - ID_CONF, TS_ANY_RW, 0), + ID_CONF, TS_ANY_RW, SUBSET_NVM), // INPUT DATA ///////////////////////////////////////////////////////////// diff --git a/test/test_shim.cpp b/test/test_shim.cpp index 0d95ff7..f212e9a 100644 --- a/test/test_shim.cpp +++ b/test/test_shim.cpp @@ -9,6 +9,6 @@ void test_shim_get_object(void) { ThingSet ts(&data_objects[0], data_objects_size); - ThingSetDataObject *object = ts.get_object(ID_INFO); + ThingSetDataObject *object = ts.get_object(0x10); // timestamp object "t_s" TEST_ASSERT_EQUAL_PTR(&data_objects[0], object); } diff --git a/test/test_txt.c b/test/test_txt.c index 508869a..7f8853a 100644 --- a/test/test_txt.c +++ b/test/test_txt.c @@ -96,13 +96,13 @@ void test_txt_patch_unknown_object(void) TEST_ASSERT_TXT_REQ("=conf {\"i3\" : 52}", ":A4 Not Found."); } -void test_txt_conf_callback(void) +void test_txt_group_callback(void) { - conf_callback_called = 0; + group_callback_called = false; TEST_ASSERT_TXT_REQ("=conf {\"i32\":52}", ":84 Changed."); - TEST_ASSERT_EQUAL(1, conf_callback_called); + TEST_ASSERT_EQUAL(true, group_callback_called); } void test_txt_exec(void) @@ -114,26 +114,46 @@ void test_txt_exec(void) TEST_ASSERT_EQUAL(1, dummy_called_flag); } +#if TS_NESTED_JSON + +void test_txt_statement_subset(void) +{ + const char expected[] = "#report {\"t_s\":12345678," + "\"meas\":{\"Bat_V\":14.10,\"Bat_A\":5.13,\"Ambient_degC\":22}}"; + + int resp_len = ts_txt_statement_by_path(&ts, (char *)resp_buf, TS_RESP_BUFFER_LEN, "report"); + + TEST_ASSERT_TXT_RESP(resp_len, expected); + + resp_len = ts_txt_statement_by_id(&ts, (char *)resp_buf, TS_RESP_BUFFER_LEN, ID_REPORT); + + TEST_ASSERT_TXT_RESP(resp_len, expected); +} + +#else + void test_txt_statement_subset(void) { int resp_len = ts_txt_statement_by_path(&ts, (char *)resp_buf, TS_RESP_BUFFER_LEN, "report"); - TEST_ASSERT_TXT_RESP(resp_len, "#report {\"Timestamp_s\":12345678,\"Bat_V\":14.10,\"Bat_A\":5.13,\"Ambient_degC\":22}"); + TEST_ASSERT_TXT_RESP(resp_len, "#report {\"t_s\":12345678,\"Bat_V\":14.10,\"Bat_A\":5.13,\"Ambient_degC\":22}"); resp_len = ts_txt_statement_by_id(&ts, (char *)resp_buf, TS_RESP_BUFFER_LEN, ID_REPORT); - TEST_ASSERT_TXT_RESP(resp_len, "#report {\"Timestamp_s\":12345678,\"Bat_V\":14.10,\"Bat_A\":5.13,\"Ambient_degC\":22}"); + TEST_ASSERT_TXT_RESP(resp_len, "#report {\"t_s\":12345678,\"Bat_V\":14.10,\"Bat_A\":5.13,\"Ambient_degC\":22}"); } +#endif /* TS_NESTED_JSON */ + void test_txt_statement_group(void) { int resp_len = ts_txt_statement_by_path(&ts, (char *)resp_buf, TS_RESP_BUFFER_LEN, "info"); - TEST_ASSERT_TXT_RESP(resp_len, "#info {\"Manufacturer\":\"Libre Solar\",\"Timestamp_s\":12345678,\"DeviceID\":\"ABCD1234\"}"); + TEST_ASSERT_TXT_RESP(resp_len, "#info {\"Manufacturer\":\"Libre Solar\",\"DeviceID\":\"ABCD1234\"}"); resp_len = ts_txt_statement_by_id(&ts, (char *)resp_buf, TS_RESP_BUFFER_LEN, ID_INFO); - TEST_ASSERT_TXT_RESP(resp_len, "#info {\"Manufacturer\":\"Libre Solar\",\"Timestamp_s\":12345678,\"DeviceID\":\"ABCD1234\"}"); + TEST_ASSERT_TXT_RESP(resp_len, "#info {\"Manufacturer\":\"Libre Solar\",\"DeviceID\":\"ABCD1234\"}"); } void test_txt_pub_list_channels(void) @@ -150,20 +170,40 @@ void test_txt_pub_enable(void) TEST_ASSERT_TRUE(pub_report_enable); } +#if TS_NESTED_JSON + +void test_txt_pub_delete_append_object(void) +{ + /* before change */ + TEST_ASSERT_TXT_REQ("?report", ":85 Content. [\"t_s\",\"meas/Bat_V\",\"meas/Bat_A\",\"meas/Ambient_degC\"]"); + /* delete "Ambient_degC" */ + TEST_ASSERT_TXT_REQ("-report \"meas/Ambient_degC\"", ":82 Deleted."); + /* check if it was deleted */ + TEST_ASSERT_TXT_REQ("?report", ":85 Content. [\"t_s\",\"meas/Bat_V\",\"meas/Bat_A\"]"); + /* append "Ambient_degC" again */ + TEST_ASSERT_TXT_REQ("+report \"meas/Ambient_degC\"", ":81 Created."); + /* check if it was appended */ + TEST_ASSERT_TXT_REQ("?report", ":85 Content. [\"t_s\",\"meas/Bat_V\",\"meas/Bat_A\",\"meas/Ambient_degC\"]"); +} + +#else + void test_txt_pub_delete_append_object(void) { /* before change */ - TEST_ASSERT_TXT_REQ("?report", ":85 Content. [\"Timestamp_s\",\"Bat_V\",\"Bat_A\",\"Ambient_degC\"]"); + TEST_ASSERT_TXT_REQ("?report", ":85 Content. [\"t_s\",\"Bat_V\",\"Bat_A\",\"Ambient_degC\"]"); /* delete "Ambient_degC" */ TEST_ASSERT_TXT_REQ("-report \"Ambient_degC\"", ":82 Deleted."); /* check if it was deleted */ - TEST_ASSERT_TXT_REQ("?report", ":85 Content. [\"Timestamp_s\",\"Bat_V\",\"Bat_A\"]"); + TEST_ASSERT_TXT_REQ("?report", ":85 Content. [\"t_s\",\"Bat_V\",\"Bat_A\"]"); /* append "Ambient_degC" again */ TEST_ASSERT_TXT_REQ("+report \"Ambient_degC\"", ":81 Created."); /* check if it was appended */ - TEST_ASSERT_TXT_REQ("?report", ":85 Content. [\"Timestamp_s\",\"Bat_V\",\"Bat_A\",\"Ambient_degC\"]"); + TEST_ASSERT_TXT_REQ("?report", ":85 Content. [\"t_s\",\"Bat_V\",\"Bat_A\",\"Ambient_degC\"]"); } +#endif /* TS_NESTED_JSON */ + void test_txt_auth_user(void) { /* authorize as expert user */ @@ -233,9 +273,41 @@ void test_txt_get_endpoint(void) TEST_ASSERT_EQUAL_UINT16(0xE1, object->id); } +#if TS_NESTED_JSON + +void test_txt_export(void) +{ + const char expected[] = + "{\"t_s\":12345678,\"meas\":{\"Bat_V\":14.10,\"Bat_A\":5.13,\"Ambient_degC\":22}}"; + + int resp_len = ts_txt_export(&ts, (char *)resp_buf, TS_RESP_BUFFER_LEN, SUBSET_REPORT); + resp_buf[resp_len] = '\0'; + + TEST_ASSERT_TXT_RESP(resp_len, expected); +} + +#else + void test_txt_export(void) { int resp_len = ts_txt_export(&ts, (char *)resp_buf, TS_RESP_BUFFER_LEN, SUBSET_REPORT); - TEST_ASSERT_TXT_RESP(resp_len, "{\"Timestamp_s\":12345678,\"Bat_V\":14.10,\"Bat_A\":5.13,\"Ambient_degC\":22}"); + TEST_ASSERT_TXT_RESP(resp_len, "{\"t_s\":12345678,\"Bat_V\":14.10,\"Bat_A\":5.13,\"Ambient_degC\":22}"); +} + +#endif /* TS_NESTED_JSON */ + +void test_txt_update_callback(void) +{ + update_callback_called = false; + + // without callback + ts_set_update_callback(&ts, SUBSET_NVM, NULL); + TEST_ASSERT_TXT_REQ("=conf {\"BatCharging_V\":52}", ":84 Changed."); + TEST_ASSERT_EQUAL(false, update_callback_called); + + // with configured callback + ts_set_update_callback(&ts, SUBSET_NVM, update_callback); + TEST_ASSERT_TXT_REQ("=conf {\"BatCharging_V\":52}", ":84 Changed."); + TEST_ASSERT_EQUAL(true, update_callback_called); } diff --git a/zephyr/Kconfig.thingset b/zephyr/Kconfig.thingset index d744f28..5e3e0a7 100644 --- a/zephyr/Kconfig.thingset +++ b/zephyr/Kconfig.thingset @@ -58,6 +58,23 @@ config THINGSET_CPP_LEGACY ThingSet protocol library. Enable if your C++ code uses DataNode or ArrayInfo instead of ThingSetDataNode or ThingSetArrayInfo. +config THINGSET_NESTED_JSON + bool "Use nested JSON for statements" + default n + help + The ThingSet specification v0.5 introduces a different data layout compared to previous + versions where the data is grouped by entities of the device (like battery, actuator) + instead of the data type (e.g. measurement, configuration). The data type is described by + a single character prefix in the item name. + The new grouping allows to have same item names in different groups, so for unambigous + description of the data the nested structure has to be maintained in statements. + + With nested JSON enabled, requesting names for IDs will also return the entire path (as + a JSON pointer) instead of just the data item name. + + This option is introduced to maintain compatibility with legacy firmware and will be + enabled by default in the future. + module = THINGSET module-str = thingset source "subsys/logging/Kconfig.template.log_config" diff --git a/zephyr/tests/src/main.c b/zephyr/tests/src/main.c index ea0db66..759cab4 100644 --- a/zephyr/tests/src/main.c +++ b/zephyr/tests/src/main.c @@ -47,7 +47,7 @@ void test_main(void) ztest_unit_test_setup_teardown(test_txt_patch_readonly, setup, teardown), ztest_unit_test_setup_teardown(test_txt_patch_wrong_path, setup, teardown), ztest_unit_test_setup_teardown(test_txt_patch_unknown_object, setup, teardown), - ztest_unit_test_setup_teardown(test_txt_conf_callback, setup, teardown), + ztest_unit_test_setup_teardown(test_txt_group_callback, setup, teardown), /* Text mode: POST request */ ztest_unit_test_setup_teardown(test_txt_exec, setup, teardown), /* Text mode: statements (pub/sub messages) */ @@ -66,6 +66,8 @@ void test_main(void) ztest_unit_test_setup_teardown(test_txt_get_endpoint, setup, teardown), /* Text mode: exporting of data */ ztest_unit_test_setup_teardown(test_txt_export, setup, teardown), + /* Text mode: update notification */ + ztest_unit_test_setup_teardown(test_txt_update_callback, setup, teardown), /* Bin mode: GET request */ ztest_unit_test_setup_teardown(test_bin_get_meas_ids_values, setup, teardown), @@ -98,7 +100,9 @@ void test_main(void) #endif /* Bin mode: exporting/importing of data */ ztest_unit_test_setup_teardown(test_bin_export, setup, teardown), - ztest_unit_test_setup_teardown(test_bin_import, setup, teardown) + ztest_unit_test_setup_teardown(test_bin_import, setup, teardown), + /* Bin mode: update notification */ + ztest_unit_test_setup_teardown(test_bin_update_callback, setup, teardown) ); ztest_run_test_suite(thingset_tests);