From 5e5a6d82f7a2e89490cbe6d8328f94bc65891cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tja=C5=BE=20Vra=C4=8Dko?= Date: Thu, 11 May 2023 11:50:26 +0200 Subject: [PATCH 1/8] Add user_settings_set_changed_with_* functions --- library/include/user_settings.h | 22 ++++++++++++++++++++++ library/user_settings/user_settings.c | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/library/include/user_settings.h b/library/include/user_settings.h index 573cf4b..67b94c1 100644 --- a/library/include/user_settings.h +++ b/library/include/user_settings.h @@ -506,6 +506,28 @@ bool user_settings_iter_next(char **key, uint16_t *id); */ bool user_settings_iter_next_changed(char **key, uint16_t *id); +/** + * @brief Set the "has_changed_recently" flag + * + * This will assert if no setting with the provided key exists. + * If the key input for this function is unknown to the application (i.e. parsed from user), then + * it should first be checked with user_settings_exists_with_key(). + * + * @param[in] key A valid user setting key + */ +void user_settings_set_changed_with_key(char *key); + +/** + * @brief Set the "has_changed_recently" flag + * + * This will assert if no setting with the provided id exists. + * If the ID input for this function is unknown to the application (i.e. parsed from user), then + * it should first be checked with user_settings_exists_with_id(). + * + * @param[in] key A valid user setting id + */ +void user_settings_set_changed_with_id(uint16_t id); + /** * @brief Clear "has_changed_recently" flag * diff --git a/library/user_settings/user_settings.c b/library/user_settings/user_settings.c index 466f35c..38c2349 100644 --- a/library/user_settings/user_settings.c +++ b/library/user_settings/user_settings.c @@ -633,6 +633,26 @@ bool user_settings_iter_next_changed(char **key, uint16_t *id) return true; } +void user_settings_set_changed_with_key(char *key) +{ + __ASSERT(prv_is_loaded, LOAD_ASSERT_TEXT); + + struct user_setting *s = user_settings_list_get_by_key(key); + __ASSERT(s, "Key does not exists: %s", key); + + s->has_changed_recently = 1; +} + +void user_settings_set_changed_with_id(uint16_t id) +{ + __ASSERT(prv_is_loaded, LOAD_ASSERT_TEXT); + + struct user_setting *s = user_settings_list_get_by_id(id); + __ASSERT(s, "Id does not exists: %d", id); + + s->has_changed_recently = 1; +} + void user_settings_clear_changed_with_key(char *key) { __ASSERT(prv_is_loaded, LOAD_ASSERT_TEXT); From e73df2261c69d9b0b8f38ef7f411b899c7e75727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tja=C5=BE=20Vra=C4=8Dko?= Date: Thu, 11 May 2023 11:51:49 +0200 Subject: [PATCH 2/8] Add option to always mark changed settings as changed to user_settings_set_from_json. Previously, a setting was only marked changed if the new value was different. Now, a bool parameter controls the behaviour. --- library/include/user_settings_json.h | 5 +- library/user_settings/user_settings_json.c | 55 +++++++++++++++------- 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/library/include/user_settings_json.h b/library/include/user_settings_json.h index 42732c6..5a3d4c8 100644 --- a/library/include/user_settings_json.h +++ b/library/include/user_settings_json.h @@ -32,13 +32,16 @@ extern "C" { * } * * @param[in] settings The settings to apply + * @param[in] always_mark_changed If true, always mark settings as changed, even if the new value is + * the same as the old one. If false, a setting will only be marked + * changed if the new value is different from the old one. * * @retval 0 On success * @retval -ENOMEM If the new value is larger than the max_size * @retval -EIO if the setting value could not be stored to NVS * @retval -EINVAL if the invalid json structure */ -int user_settings_set_from_json(const cJSON *settings); +int user_settings_set_from_json(const cJSON *settings, bool always_mark_changed); /** * @brief Create a JSON with containing only settings marked changed. diff --git a/library/user_settings/user_settings_json.c b/library/user_settings/user_settings_json.c index 7210136..522a1fb 100644 --- a/library/user_settings/user_settings_json.c +++ b/library/user_settings/user_settings_json.c @@ -25,84 +25,88 @@ LOG_MODULE_REGISTER(user_settings_json, CONFIG_USER_SETTINGS_LOG_LEVEL); * @brief Set value from JSON structure. * Function expects we have already checked that setting key and value are valid. * - * @param type - setting type - * @param setting - setting to set. + * @param[in] type - setting type + * @param[in] setting - setting to set. + * @param[in] always_mark_changed If true, always mark settings as changed, even if the new value is + * the same as the old one. + * * @retval 0 On success * @retval -ENOMEM If the new value is larger than the max_size * @retval -EIO if the setting value could not be stored to NVS * @retval -EINVAL if the invalid data format is provided in json structure */ -static int prv_set_from_json(enum user_setting_type type, cJSON *setting) +static int prv_set_from_json(enum user_setting_type type, cJSON *setting, bool always_mark_changed) { + int err = -EINVAL; switch (type) { case USER_SETTINGS_TYPE_BOOL: { if (cJSON_IsBool(setting)) { bool v = cJSON_IsTrue(setting); - return user_settings_set_with_key(setting->string, &v, sizeof(v)); + err = user_settings_set_with_key(setting->string, &v, sizeof(v)); } break; } case USER_SETTINGS_TYPE_U8: { if (cJSON_IsNumber(setting)) { uint8_t v = (uint8_t)setting->valueint; - return user_settings_set_with_key(setting->string, &v, sizeof(v)); + err = user_settings_set_with_key(setting->string, &v, sizeof(v)); } break; } case USER_SETTINGS_TYPE_U16: { if (cJSON_IsNumber(setting)) { uint16_t v = (uint16_t)setting->valueint; - return user_settings_set_with_key(setting->string, &v, sizeof(v)); + err = user_settings_set_with_key(setting->string, &v, sizeof(v)); } break; } case USER_SETTINGS_TYPE_U32: { if (cJSON_IsNumber(setting)) { uint32_t v = (uint32_t)setting->valueint; - return user_settings_set_with_key(setting->string, &v, sizeof(v)); + err = user_settings_set_with_key(setting->string, &v, sizeof(v)); } break; } case USER_SETTINGS_TYPE_U64: { if (cJSON_IsNumber(setting)) { uint64_t v = (uint64_t)setting->valueint; - return user_settings_set_with_key(setting->string, &v, sizeof(v)); + err = user_settings_set_with_key(setting->string, &v, sizeof(v)); } break; } case USER_SETTINGS_TYPE_I8: { if (cJSON_IsNumber(setting)) { int8_t v = (int8_t)setting->valueint; - return user_settings_set_with_key(setting->string, &v, sizeof(v)); + err = user_settings_set_with_key(setting->string, &v, sizeof(v)); } break; } case USER_SETTINGS_TYPE_I16: { if (cJSON_IsNumber(setting)) { int16_t v = (int16_t)setting->valueint; - return user_settings_set_with_key(setting->string, &v, sizeof(v)); + err = user_settings_set_with_key(setting->string, &v, sizeof(v)); } break; } case USER_SETTINGS_TYPE_I32: { if (cJSON_IsNumber(setting)) { int32_t v = (int32_t)setting->valueint; - return user_settings_set_with_key(setting->string, &v, sizeof(v)); + err = user_settings_set_with_key(setting->string, &v, sizeof(v)); } break; } case USER_SETTINGS_TYPE_I64: { if (cJSON_IsNumber(setting)) { int64_t v = (int64_t)setting->valueint; - return user_settings_set_with_key(setting->string, &v, sizeof(v)); + err = user_settings_set_with_key(setting->string, &v, sizeof(v)); } break; } case USER_SETTINGS_TYPE_STR: { if (cJSON_IsString(setting)) { char *v = setting->valuestring; - return user_settings_set_with_key(setting->string, v, - strlen(setting->valuestring) + 1); + err = user_settings_set_with_key(setting->string, v, + strlen(setting->valuestring) + 1); } break; } @@ -116,16 +120,31 @@ static int prv_set_from_json(enum user_setting_type type, cJSON *setting) bytes[i] = (setting->valuestring[j] % 32 + 9) % 25 * 16 + (setting->valuestring[j + 1] % 32 + 9) % 25; } - return user_settings_set_with_key(setting->string, bytes, bytes_len); + err = user_settings_set_with_key(setting->string, bytes, bytes_len); } break; } default: { LOG_ERR("Type not supported!"); + err = -EINVAL; + } + } + + if (err) { + return err; } + + /* user_settings_set_with_key() only marks a setting as changed if the new value is + * different. If we want to always mark them, we have do it explicitly here. + * + * If the new value is different, the setting will be marked as changed twice, but this is + * not a problem. + */ + if (always_mark_changed) { + user_settings_set_changed_with_key(setting->string); } - return -EINVAL; + return 0; } /** @@ -208,7 +227,7 @@ static cJSON *prv_json_from_setting(struct user_setting *setting) return json_setting; } -int user_settings_set_from_json(const cJSON *settings) +int user_settings_set_from_json(const cJSON *settings, bool always_mark_changed) { int err; @@ -238,7 +257,7 @@ int user_settings_set_from_json(const cJSON *settings) /* Get stored setting type */ type = user_settings_get_type_with_key(setting->string); - err = prv_set_from_json(type, setting); + err = prv_set_from_json(type, setting, always_mark_changed); if (err == -EINVAL) { LOG_ERR("Invalid json data for setting: %s", setting->string); } else if (err) { From 19fdc729929a8bc14880c3cee29dc40c6479c004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tja=C5=BE=20Vra=C4=8Dko?= Date: Thu, 11 May 2023 12:05:07 +0200 Subject: [PATCH 3/8] Fix docstrings in user_settings_json.h --- library/include/user_settings_json.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/include/user_settings_json.h b/library/include/user_settings_json.h index 5a3d4c8..995761b 100644 --- a/library/include/user_settings_json.h +++ b/library/include/user_settings_json.h @@ -45,9 +45,10 @@ int user_settings_set_from_json(const cJSON *settings, bool always_mark_changed) /** * @brief Create a JSON with containing only settings marked changed. - * Function will not clear changed flag. * - * The caller is expected to free the created cJSON structure. + * Calling this function will not clear the changed flag of any user setting. + * + * The caller is expected to free the created cJSON structure. * * @param[out] settings Created json * @retval 0 On success @@ -58,7 +59,7 @@ int user_settings_get_changed_json(cJSON **settings); /** * @brief Create a JSON with containing all settings. * - * The caller is expected to free the created cJSON structure. + * The caller is expected to free the created cJSON structure. * * @param[out] settings Created json * @retval 0 On success From cef78d8ebe6b216fecc19ce726096e1ab3531c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tja=C5=BE=20Vra=C4=8Dko?= Date: Thu, 11 May 2023 12:05:23 +0200 Subject: [PATCH 4/8] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 497ab1e..9f307ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +### Added + +- user_settings_set_changed_with_key and user_settings_set_changed_with_id functions. +- always_mark_changed parameter to user_settings_set_from_json function. + ## [1.5.0] - 2023-04-11 ### Added From 6b829f40bb4157a1fc7162aed79bdadbd2098284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tja=C5=BE=20Vra=C4=8Dko?= Date: Thu, 11 May 2023 14:10:05 +0200 Subject: [PATCH 5/8] Store has_changed_recently flag in NVS --- library/user_settings/user_settings.c | 116 +++++++++++++++++++++++--- 1 file changed, 106 insertions(+), 10 deletions(-) diff --git a/library/user_settings/user_settings.c b/library/user_settings/user_settings.c index 38c2349..0999f90 100644 --- a/library/user_settings/user_settings.c +++ b/library/user_settings/user_settings.c @@ -22,8 +22,9 @@ LOG_MODULE_REGISTER(user_settings, CONFIG_USER_SETTINGS_LOG_LEVEL); #define INIT_ASSERT_TEXT "user_settings_init should be called before this function" #define LOAD_ASSERT_TEXT "user_settings_load should be called before this function" -#define USER_SETTINGS_PREFIX "user" -#define USER_SETTINGS_DEFAULT_PREFIX "user_default" +#define USER_SETTINGS_PREFIX "user" +#define USER_SETTINGS_DEFAULT_PREFIX "user_default" +#define USER_SETTINGS_CHANGED_FLAG_PREFIX "user_changed" /* External callback */ static user_settings_on_change_t prv_global_on_change_cb; @@ -117,6 +118,36 @@ static int prv_value_set_cb(const char *key, size_t len, settings_read_cb read_c return 0; } +/** + * @brief This is called when we call settings_runtime_set on the changed prefix + */ +static int prv_changed_flag_set_cb(const char *key, size_t len, settings_read_cb read_cb, + void *cb_arg) +{ + int rc; + + /* Check if key exists in the settings list */ + struct user_setting *setting = user_settings_list_get_by_key(key); + if (!setting) { + return -ENOENT; + } + + /* Read the flag from NVS */ + rc = read_cb(cb_arg, &setting->has_changed_recently, sizeof(setting->has_changed_recently)); + if (rc < 0) { + LOG_ERR("read_cb, err: %d", rc); + return rc; + } else if (rc == 0) { + LOG_ERR("read_cb, this key value pair was deleted"); + return 0; + } + + LOG_DBG("Setting %s has_changed_recently flag was read: %d", setting->key, + setting->has_changed_recently); + + return 0; +} + int user_settings_init(void) { static struct settings_handler prv_default_sh = { @@ -129,6 +160,11 @@ int user_settings_init(void) .h_set = prv_value_set_cb, }; + static struct settings_handler prv_changed_sh = { + .name = USER_SETTINGS_CHANGED_FLAG_PREFIX, + .h_set = prv_changed_flag_set_cb, + }; + __ASSERT(!prv_is_inited, "user_settings_init should only be called once"); int err; @@ -156,6 +192,13 @@ int user_settings_init(void) return -EIO; } + /* register handler for changed flag */ + err = settings_register(&prv_changed_sh); + if (err) { + LOG_ERR("settings_register, err: %d", err); + return -EIO; + } + prv_is_inited = true; return 0; @@ -183,8 +226,6 @@ int user_settings_load(void) { __ASSERT(prv_is_inited, INIT_ASSERT_TEXT); - prv_is_loaded = true; - int err; /* load all default values */ @@ -201,6 +242,15 @@ int user_settings_load(void) return -EIO; } + /* load changed recently flags */ + err = settings_load_subtree(USER_SETTINGS_CHANGED_FLAG_PREFIX); + if (err) { + LOG_ERR("Failed loading user_settings_changed_recently subtree, err: %d", err); + return -EIO; + } + + prv_is_loaded = true; + return 0; } @@ -272,6 +322,48 @@ int user_settings_set_default_with_id(uint16_t id, void *data, size_t len) return prv_user_settings_set_default(s, data, len); } +/** + * @brief Persistently set the changed recently flag for a setting. + * + * @param[in] s The setting to set the flag for. + * @param[in] has_changed_recently The value of the flag. + * + * @return int 0 on success, negative errno code otherwise. + */ +static int prv_set_changed_recently_flag(struct user_setting *s, bool has_changed_recently) +{ + __ASSERT(prv_is_loaded, LOAD_ASSERT_TEXT); + + int err; + + /* Check if value is the same */ + if (has_changed_recently == s->has_changed_recently) { + LOG_DBG("Setting has_changed_recently flag to same value."); + return 0; + } + + /* Use settings_runtime_set() so that prv_changed_flag_set_cb gets called, which will + * set the flag in the settings list */ + char key_with_prefix[SETTINGS_MAX_NAME_LEN + 1] = {0}; + sprintf(key_with_prefix, USER_SETTINGS_CHANGED_FLAG_PREFIX "/%s", s->key); + err = settings_runtime_set(key_with_prefix, &has_changed_recently, + sizeof(has_changed_recently)); + if (err) { + LOG_ERR("settings_runtime_set, err: %d", err); + return -EIO; + } + + /* Use settings_save_one() so that the flag is stored to NVS */ + err = settings_save_one(key_with_prefix, &has_changed_recently, + sizeof(has_changed_recently)); + if (err) { + LOG_ERR("settings_save, err: %d", err); + return -EIO; + } + + return 0; +} + static int prv_user_settings_set(struct user_setting *s, void *data, size_t len) { __ASSERT(prv_is_loaded, LOAD_ASSERT_TEXT); @@ -308,7 +400,11 @@ static int prv_user_settings_set(struct user_setting *s, void *data, size_t len) } /* Modify has changed flag */ - s->has_changed_recently = 1; + err = prv_set_changed_recently_flag(s, true); + if (err) { + LOG_ERR("prv_set_changed_recently_flag, err: %d", err); + return -EIO; + } return 0; } @@ -640,7 +736,7 @@ void user_settings_set_changed_with_key(char *key) struct user_setting *s = user_settings_list_get_by_key(key); __ASSERT(s, "Key does not exists: %s", key); - s->has_changed_recently = 1; + prv_set_changed_recently_flag(s, true); } void user_settings_set_changed_with_id(uint16_t id) @@ -650,7 +746,7 @@ void user_settings_set_changed_with_id(uint16_t id) struct user_setting *s = user_settings_list_get_by_id(id); __ASSERT(s, "Id does not exists: %d", id); - s->has_changed_recently = 1; + prv_set_changed_recently_flag(s, true); } void user_settings_clear_changed_with_key(char *key) @@ -660,7 +756,7 @@ void user_settings_clear_changed_with_key(char *key) struct user_setting *s = user_settings_list_get_by_key(key); __ASSERT(s, "Key does not exists: %s", key); - s->has_changed_recently = 0; + prv_set_changed_recently_flag(s, 0); } void user_settings_clear_changed_with_id(uint16_t id) @@ -670,7 +766,7 @@ void user_settings_clear_changed_with_id(uint16_t id) struct user_setting *s = user_settings_list_get_by_id(id); __ASSERT(s, "Id does not exists: %d", id); - s->has_changed_recently = 0; + prv_set_changed_recently_flag(s, 0); } void user_settings_clear_changed(void) @@ -680,7 +776,7 @@ void user_settings_clear_changed(void) user_settings_list_iter_start(); struct user_setting *setting; while ((setting = user_settings_list_iter_next()) != NULL) { - setting->has_changed_recently = 0; + prv_set_changed_recently_flag(setting, 0); } } From 3fd5360cc9b40fd6efea4fbf2091df77d9c3fc46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tja=C5=BE=20Vra=C4=8Dko?= Date: Thu, 11 May 2023 14:10:48 +0200 Subject: [PATCH 6/8] Add shell commands related to the changed flag --- library/user_settings/user_settings_shell.c | 40 ++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/library/user_settings/user_settings_shell.c b/library/user_settings/user_settings_shell.c index 7fb589d..74b01d7 100644 --- a/library/user_settings/user_settings_shell.c +++ b/library/user_settings/user_settings_shell.c @@ -117,6 +117,19 @@ static int cmd_list(const struct shell *shell_ptr, size_t argc, char *argv[]) return 0; } +static int cmd_list_changed(const struct shell *shell_ptr, size_t argc, char *argv[]) +{ + user_settings_list_iter_start(); + struct user_setting *setting; + while ((setting = user_settings_list_iter_next()) != NULL) { + if (setting->has_changed_recently) { + prv_shell_print_setting(shell_ptr, setting); + } + } + + return 0; +} + static int cmd_get(const struct shell *shell_ptr, size_t argc, char *argv[]) { const char *key = argv[1]; @@ -245,6 +258,26 @@ static int cmd_restore_one(const struct shell *shell_ptr, size_t argc, char *arg return 0; } +static int cmd_clear_changed(const struct shell *shell_ptr, size_t argc, char *argv[]) +{ + user_settings_clear_changed(); + return 0; +} + +static int cmd_clear_changed_one(const struct shell *shell_ptr, size_t argc, char *argv[]) +{ + char *key = argv[1]; + + struct user_setting *s = user_settings_list_get_by_key(key); + if (!s) { + shell_error(shell_ptr, "Setting with this key not found: %s", key); + return -ENOENT; + } + + user_settings_clear_changed_with_key(s->key); + return 0; +} + /** * @brief Get user setting at position @p idx in the list * @@ -281,13 +314,14 @@ static void prv_shell_key_get(size_t idx, struct shell_static_entry *entry) entry->subcmd = NULL; } -// create a dinamic set of subcommands. This is called every time +// create a dynamic set of subcommands. This is called every time // "voltage_divider get" is invoked in a shell and will create a list of devices // with @device_name_get SHELL_DYNAMIC_CMD_CREATE(dsub_setting_key, prv_shell_key_get); SHELL_STATIC_SUBCMD_SET_CREATE( settings_cmds, SHELL_CMD_ARG(list, NULL, "list all user settings", cmd_list, 1, 0), + SHELL_CMD_ARG(list_changed, NULL, "list all user settings", cmd_list_changed, 1, 0), SHELL_CMD_ARG(get, &dsub_setting_key, " List one user setting", cmd_get, 2, 0), SHELL_CMD_ARG(set, &dsub_setting_key, " Set a user setting", cmd_set, 3, 0), SHELL_CMD_ARG(set_default, &dsub_setting_key, @@ -296,6 +330,10 @@ SHELL_STATIC_SUBCMD_SET_CREATE( SHELL_CMD_ARG(restore, NULL, "Restore all settings to default values", cmd_restore, 1, 0), SHELL_CMD_ARG(restore_one, NULL, "Restore one setting to its default value", cmd_restore_one, 2, 0), + SHELL_CMD_ARG(clear_changed, NULL, "Clear the changed flag for all settings", + cmd_clear_changed, 1, 0), + SHELL_CMD_ARG(clear_changed_one, NULL, "Clear the changed flag for one setting", + cmd_clear_changed_one, 2, 0), SHELL_SUBCMD_SET_END); static int cmd_settings(const struct shell *shell_ptr, size_t argc, char **argv) From ad7b02438f32d6182b6fa935161cd442c3ee1f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tja=C5=BE=20Vra=C4=8Dko?= Date: Thu, 11 May 2023 14:10:54 +0200 Subject: [PATCH 7/8] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f307ed..754c5cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - user_settings_set_changed_with_key and user_settings_set_changed_with_id functions. - always_mark_changed parameter to user_settings_set_from_json function. +- The "has_changed_recently" flag is now stored persistently. +- Shell commands to list only changed settings and to clear the changed flag. ## [1.5.0] - 2023-04-11 From 442df0fa266dc4a358601d281ed4d6eb5d68ac47 Mon Sep 17 00:00:00 2001 From: "github-bot :robot" Date: Fri, 12 May 2023 08:14:38 +0000 Subject: [PATCH 8/8] Update CHANGELOG.md for Release v1.6.0 --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 754c5cf..c7772fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +## [1.6.0] - 2023-05-12 + ### Added - user_settings_set_changed_with_key and user_settings_set_changed_with_id functions. @@ -97,7 +99,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Basic sample. - Callbacks sample. -[Unreleased]: https://github.com/IRNAS/irnas-usersettings-lib/compare/v1.5.0...HEAD +[Unreleased]: https://github.com/IRNAS/irnas-usersettings-lib/compare/v1.6.0...HEAD + +[1.6.0]: https://github.com/IRNAS/irnas-usersettings-lib/compare/v1.5.0...v1.6.0 [1.5.0]: https://github.com/IRNAS/irnas-usersettings-lib/compare/v1.4.0...v1.5.0