diff --git a/app/src/main.c b/app/src/main.c index a349a2b4..c9beed0a 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1927,8 +1927,20 @@ static void fota_rebooting_entry(void *o) { ARG_UNUSED(o); + struct storage_msg msg = { .type = STORAGE_CLEAR }; + int err; + LOG_DBG("%s", __func__); + /* Tell storage module to clear any stored data */ + err = zbus_chan_pub(&STORAGE_CHAN, &msg, K_MSEC(ZBUS_PUBLISH_TIMEOUT_MS)); + if (err) { + LOG_ERR("Failed to publish storage clear message, error: %d", err); + SEND_FATAL_ERROR(); + + return; + } + /* Reboot the device */ LOG_WRN("Rebooting the device to apply the FOTA update"); diff --git a/app/src/modules/cloud/Kconfig.cloud b/app/src/modules/cloud/Kconfig.cloud index addbf195..35fc03e3 100644 --- a/app/src/modules/cloud/Kconfig.cloud +++ b/app/src/modules/cloud/Kconfig.cloud @@ -80,6 +80,35 @@ config APP_CLOUD_BACKOFF_MAX_SECONDS help Maximum reconnection backoff value in seconds. +choice APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS + prompt "Handling of wrong timestamps in data samples" + default APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_DROP + +config APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_DROP + bool "Drop data samples with wrong timestamps" + help + Data samples with wrong timestamps will be dropped and not sent to the cloud. + +config APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_KEEP + bool "Keep data samples with wrong timestamps" + help + Data samples with wrong timestamps will be kept and sent to the cloud as is. + +config APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_NOW + bool "Set timestamp to current time" + help + Data samples with wrong timestamps will have their timestamps set to the current time + before being sent to the cloud. + +config APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_NO_TIMESTAMP + bool "Set timestamp to NO_TIMESTAMP" + help + Data samples with wrong timestamps will have their timestamps set to NO_TIMESTAMP + before being sent to the cloud. For nRF Cloud, this means the cloud will set the timestamp + to the time of reception. + +endchoice + config APP_CLOUD_THREAD_STACK_SIZE int "Thread stack size" default 5120 diff --git a/app/src/modules/cloud/cloud.c b/app/src/modules/cloud/cloud.c index 136e6aaf..91eebfb9 100644 --- a/app/src/modules/cloud/cloud.c +++ b/app/src/modules/cloud/cloud.c @@ -337,6 +337,36 @@ static void send_request_failed(void) } } +static int handle_data_timestamp(int64_t *timestamp_ms) +{ + int err; + + /* Soft attempt to convert uptime to unix time, keep original value on failure */ + err = attempt_timestamp_to_unix_ms(timestamp_ms); + if (err == 0 || err == -EALREADY) { + return 0; + } + + if (IS_ENABLED(CONFIG_APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_KEEP)) { + LOG_WRN("Keeping original timestamp value"); + return 0; + } else if (IS_ENABLED(CONFIG_APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_NOW)) { + *timestamp_ms = k_uptime_get(); + err = attempt_timestamp_to_unix_ms(timestamp_ms); + if (err) { + LOG_ERR("Failed to set timestamp to current time, error: %d", err); + return err; + } + LOG_WRN("Setting timestamp to current time"); + return 0; + } else if (IS_ENABLED(CONFIG_APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_NO_TIMESTAMP)) { + *timestamp_ms = NRF_CLOUD_NO_TIMESTAMP; + return 0; + } else { /* Default behavior: APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_DROP */ + return err; + } +} + static void handle_network_data_message(const struct network_msg *msg) { int err; @@ -347,11 +377,11 @@ static void handle_network_data_message(const struct network_msg *msg) return; } - /* Convert uptime to unix time */ - timestamp_ms = msg->uptime; - err = date_time_uptime_to_unix_time_ms(×tamp_ms); + /* Convert timestamp to unix time */ + timestamp_ms = msg->timestamp; + err = handle_data_timestamp(×tamp_ms); if (err) { - LOG_ERR("date_time_uptime_to_unix_time_ms, error: %d", err); + return; } err = nrf_cloud_coap_sensor_send(CUSTOM_JSON_APPID_VAL_CONEVAL, @@ -387,11 +417,11 @@ static int send_storage_data_to_cloud(const struct storage_data_item *item) if (item->type == STORAGE_TYPE_BATTERY) { const struct power_msg *power = &item->data.BATTERY; - /* Convert uptime to unix time */ - timestamp_ms = power->uptime; - err = date_time_uptime_to_unix_time_ms(×tamp_ms); + /* Convert timestamp to unix time */ + timestamp_ms = power->timestamp; + err = handle_data_timestamp(×tamp_ms); if (err) { - LOG_ERR("date_time_uptime_to_unix_time_ms, error: %d", err); + return err; } err = nrf_cloud_coap_sensor_send(CUSTOM_JSON_APPID_VAL_BATTERY, @@ -416,11 +446,11 @@ static int send_storage_data_to_cloud(const struct storage_data_item *item) if (item->type == STORAGE_TYPE_ENVIRONMENTAL) { const struct environmental_msg *env = &item->data.ENVIRONMENTAL; - /* Convert uptime to unix time */ - timestamp_ms = env->uptime; - err = date_time_uptime_to_unix_time_ms(×tamp_ms); + /* Convert timestamp to unix time */ + timestamp_ms = env->timestamp; + err = handle_data_timestamp(×tamp_ms); if (err) { - LOG_ERR("date_time_uptime_to_unix_time_ms, error: %d", err); + return err; } return cloud_environmental_send(env, timestamp_ms, confirmable); diff --git a/app/src/modules/cloud/cloud.h b/app/src/modules/cloud/cloud.h index 3cf65f53..bb81725a 100644 --- a/app/src/modules/cloud/cloud.h +++ b/app/src/modules/cloud/cloud.h @@ -9,6 +9,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -127,6 +128,46 @@ struct cloud_msg { #define MSG_TO_CLOUD_MSG_PTR(_msg) ((const struct cloud_msg *)_msg) +#define UNIX_TIME_MS_2026_01_01 1767222000000LL +/** + * @brief Attempt to set the provided uptime (in milliseconds) to unix time. + * + * Tries to convert the provided timestamp from uptime to unix time in milliseconds, if needed. + * If it cant convert it will stay unchanged. + * + * @param uptime_ms Uptime to convert to unix time. + * @return int 0 if conversion was successful, + * -EINVAL if the provided pointer is NULL, + * -EALREADY if the provided time was already in unix time (>= 2026-01-01), + * -ENODATA if date time is not valid, + */ +static inline int64_t attempt_timestamp_to_unix_ms(int64_t *uptime_ms) +{ + int err; + + if (uptime_ms == NULL) { + return -EINVAL; + } + if (*uptime_ms >= UNIX_TIME_MS_2026_01_01) { + /* Already unix time */ + return -EALREADY; + } + if (*uptime_ms > k_uptime_get()) { + /* Uptime cannot be in the future */ + return -EINVAL; + } + + if (!date_time_is_valid()) { + /* Cannot convert without valid time */ + return -ENODATA; + } + err = date_time_uptime_to_unix_time_ms(uptime_ms); + if (err) { + return err; + } + return 0; +} + #ifdef __cplusplus } #endif diff --git a/app/src/modules/cloud/cloud_location.c b/app/src/modules/cloud/cloud_location.c index 5be88ac4..97465bdd 100644 --- a/app/src/modules/cloud/cloud_location.c +++ b/app/src/modules/cloud/cloud_location.c @@ -263,7 +263,7 @@ static void handle_gnss_location_data(const struct location_msg *location_msg) const struct location_data *location_data = &location_msg->gnss_data; /* Convert uptime to unix time */ - timestamp_ms = location_msg->uptime; + timestamp_ms = location_msg->timestamp; err = date_time_uptime_to_unix_time_ms(×tamp_ms); if (err) { LOG_ERR("date_time_uptime_to_unix_time_ms, error: %d", err); diff --git a/app/src/modules/environmental/environmental.c b/app/src/modules/environmental/environmental.c index 701ff32e..e24f8626 100644 --- a/app/src/modules/environmental/environmental.c +++ b/app/src/modules/environmental/environmental.c @@ -10,6 +10,7 @@ #include #include #include +#include #include "app_common.h" #include "environmental.h" @@ -118,9 +119,16 @@ static void sample_sensors(const struct device *const bme680) .temperature = sensor_value_to_double(&temp), .pressure = sensor_value_to_double(&press), .humidity = sensor_value_to_double(&humidity), - .uptime = k_uptime_get(), + .timestamp = k_uptime_get(), }; + err = date_time_now(&msg.timestamp); + if (err != 0 && err != -ENODATA) { + LOG_ERR("date_time_now, error: %d", err); + SEND_FATAL_ERROR(); + return; + } + /* Log the environmental values and limit to 2 decimals */ LOG_DBG("Temperature: %.2f C, Pressure: %.2f Pa, Humidity: %.2f %%", msg.temperature, msg.pressure, msg.humidity); diff --git a/app/src/modules/environmental/environmental.h b/app/src/modules/environmental/environmental.h index fcdfdf9e..7035612c 100644 --- a/app/src/modules/environmental/environmental.h +++ b/app/src/modules/environmental/environmental.h @@ -48,11 +48,12 @@ struct environmental_msg { /** Contains the current pressure in Pa. */ double pressure; - /** Uptime when the sample was taken in milliseconds. - * Use date_time_uptime_to_unix_time_ms() to convert to unix time before sending to cloud. - * Only valid for ENVIRONMENTAL_SENSOR_SAMPLE_RESPONSE events. + /** Timestamp when the sample was taken in milliseconds. + * This is either: + * - Unix time in milliseconds if the system clock was synchronized at sampling time, or + * - Uptime in milliseconds if the system clock was not synchronized at sampling time. */ - int64_t uptime; + int64_t timestamp; }; #define MSG_TO_ENVIRONMENTAL_MSG(_msg) (*(const struct environmental_msg *)_msg) diff --git a/app/src/modules/location/location.c b/app/src/modules/location/location.c index 8c51d931..120c576b 100644 --- a/app/src/modules/location/location.c +++ b/app/src/modules/location/location.c @@ -229,9 +229,16 @@ static void gnss_location_send(const struct location_data *location_data) struct location_msg location_msg = { .type = LOCATION_GNSS_DATA, .gnss_data = *location_data, - .uptime = k_uptime_get() + .timestamp = k_uptime_get() }; + err = date_time_now(&location_msg.timestamp); + if (err != 0 && err != -ENODATA) { + LOG_ERR("date_time_now, error: %d", err); + SEND_FATAL_ERROR(); + return; + } + err = zbus_chan_pub(&LOCATION_CHAN, &location_msg, K_SECONDS(1)); if (err) { LOG_ERR("zbus_chan_pub, error: %d", err); diff --git a/app/src/modules/location/location.h b/app/src/modules/location/location.h index ed3db666..13e56c50 100644 --- a/app/src/modules/location/location.h +++ b/app/src/modules/location/location.h @@ -184,11 +184,12 @@ struct location_msg { struct location_data gnss_data; }; - /** Uptime when the location was obtained in milliseconds. - * Use date_time_uptime_to_unix_time_ms() to convert to unix time before sending to cloud. - * Only valid for LOCATION_GNSS_DATA events. + /** Timestamp when the sample was taken in milliseconds. + * This is either: + * - Unix time in milliseconds if the system clock was synchronized at sampling time, or + * - Uptime in milliseconds if the system clock was not synchronized at sampling time. */ - int64_t uptime; + int64_t timestamp; }; #define MSG_TO_LOCATION_TYPE(_msg) (((const struct location_msg *)_msg)->type) diff --git a/app/src/modules/network/network.c b/app/src/modules/network/network.c index 8d041a27..ccc1691b 100644 --- a/app/src/modules/network/network.c +++ b/app/src/modules/network/network.c @@ -258,9 +258,16 @@ static void sample_network_quality(void) int ret; struct network_msg msg = { .type = NETWORK_QUALITY_SAMPLE_RESPONSE, - .uptime = k_uptime_get() + .timestamp = k_uptime_get() }; + ret = date_time_now(&msg.timestamp); + if (ret != 0 && ret != -ENODATA) { + LOG_ERR("date_time_now, error: %d", ret); + SEND_FATAL_ERROR(); + return; + } + ret = lte_lc_conn_eval_params_get(&msg.conn_eval_params); if (ret == -EOPNOTSUPP) { LOG_WRN("Connection evaluation not supported in current functional mode"); diff --git a/app/src/modules/network/network.h b/app/src/modules/network/network.h index 756527fe..42f60641 100644 --- a/app/src/modules/network/network.h +++ b/app/src/modules/network/network.h @@ -142,11 +142,12 @@ struct network_msg { (struct lte_lc_conn_eval_params conn_eval_params)); }; - /** Uptime when the sample was taken in milliseconds. - * Use date_time_uptime_to_unix_time_ms() to convert to unix time before sending to cloud. - * Only valid for NETWORK_QUALITY_SAMPLE_RESPONSE events. + /** Timestamp when the sample was taken in milliseconds. + * This is either: + * - Unix time in milliseconds if the system clock was synchronized at sampling time, or + * - Uptime in milliseconds if the system clock was not synchronized at sampling time. */ - int64_t uptime; + int64_t timestamp; }; #define MSG_TO_NETWORK_MSG(_msg) (*(const struct network_msg *)_msg) diff --git a/app/src/modules/power/power.c b/app/src/modules/power/power.c index 666112ef..649b4258 100644 --- a/app/src/modules/power/power.c +++ b/app/src/modules/power/power.c @@ -18,6 +18,7 @@ #include #include #include +#include #include "lp803448_model.h" #include "app_common.h" @@ -441,9 +442,16 @@ static void sample(int64_t *ref_time) .percentage = (double)roundf(state_of_charge), .charging = charging, .voltage = (double)voltage, - .uptime = k_uptime_get() + .timestamp = k_uptime_get() }; + err = date_time_now(&msg.timestamp); + if (err != 0 && err != -ENODATA) { + LOG_ERR("date_time_now, error: %d", err); + SEND_FATAL_ERROR(); + return; + } + err = zbus_chan_pub(&POWER_CHAN, &msg, K_NO_WAIT); if (err) { LOG_ERR("zbus_chan_pub, error: %d", err); diff --git a/app/src/modules/power/power.h b/app/src/modules/power/power.h index d427a106..fe436692 100644 --- a/app/src/modules/power/power.h +++ b/app/src/modules/power/power.h @@ -47,11 +47,12 @@ struct power_msg { /** Voltage in volts. */ double voltage; - /** Uptime when the sample was taken in milliseconds. - * Use date_time_uptime_to_unix_time_ms() to convert to unix time before sending to cloud. - * Only valid for POWER_BATTERY_PERCENTAGE_SAMPLE_RESPONSE events. + /** Timestamp when the sample was taken in milliseconds. + * This is either: + * - Unix time in milliseconds if the system clock was synchronized at sampling time, or + * - Uptime in milliseconds if the system clock was not synchronized at sampling time. */ - int64_t uptime; + int64_t timestamp; }; #define MSG_TO_POWER_MSG(_msg) (*(const struct power_msg *)_msg) diff --git a/app/src/modules/storage/CMakeLists.txt b/app/src/modules/storage/CMakeLists.txt index 0d0e9001..a55476f1 100644 --- a/app/src/modules/storage/CMakeLists.txt +++ b/app/src/modules/storage/CMakeLists.txt @@ -16,6 +16,9 @@ zephyr_linker_sources(SECTIONS storage_sections.ld) target_sources_ifdef(CONFIG_APP_STORAGE_BACKEND_RAM app PRIVATE backends/ram_ring_buffer_backend.c ) +target_sources_ifdef(CONFIG_APP_STORAGE_BACKEND_LITTLEFS app PRIVATE + backends/littlefs_backend.c +) target_sources_ifdef(CONFIG_APP_STORAGE_SHELL app PRIVATE storage_shell.c diff --git a/app/src/modules/storage/Kconfig.storage b/app/src/modules/storage/Kconfig.storage index efd373ab..be606311 100644 --- a/app/src/modules/storage/Kconfig.storage +++ b/app/src/modules/storage/Kconfig.storage @@ -12,14 +12,38 @@ if APP_STORAGE config APP_STORAGE_THREAD_STACK_SIZE int "Storage module thread stack size" - default 1536 + default 1536 if APP_STORAGE_BACKEND_RAM + default 4096 if APP_STORAGE_BACKEND_LITTLEFS + +choice APP_STORAGE_BACKEND + prompt "Storage backend" + default APP_STORAGE_BACKEND_RAM + help + Select the storage backend to use for storing data samples. config APP_STORAGE_BACKEND_RAM bool "RAM storage backend" - default y help Store data in RAM. Data will be lost on power loss or reset. +config APP_STORAGE_BACKEND_LITTLEFS + bool "LittleFS storage backend" + select FILE_SYSTEM_LITTLEFS + select PM_PARTITION_REGION_LITTLEFS_EXTERNAL + help + Store data in LittleFS filesystem. Data will persist across power + loss and resets, as long as the underlying flash storage is intact. + +endchoice + +if APP_STORAGE_BACKEND_LITTLEFS + +# Setup PM partition size for LittleFS to 64KB by default +config PM_PARTITION_SIZE_LITTLEFS + default 0x10000 + +endif # APP_STORAGE_BACKEND_LITTLEFS + config APP_STORAGE_MAX_TYPES int "Maximum number of data types" default 4 @@ -97,6 +121,7 @@ endchoice config APP_STORAGE_SHELL bool "Enable storage shell commands" default y if SHELL + select FILE_SYSTEM_SHELL if APP_STORAGE_BACKEND_LITTLEFS help Enable shell commands for interacting with the storage module. This allows you to manage stored data from the command line. diff --git a/app/src/modules/storage/backends/littlefs_backend.c b/app/src/modules/storage/backends/littlefs_backend.c new file mode 100644 index 00000000..f78c4c29 --- /dev/null +++ b/app/src/modules/storage/backends/littlefs_backend.c @@ -0,0 +1,531 @@ +/* + * Copyright (c) 2025 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "storage.h" +#include "storage_backend.h" +#include "storage_data_types.h" + +#define MAX_PATH_LEN 255 + +// LOG_MODULE_DECLARE(storage, CONFIG_APP_STORAGE_LOG_LEVEL); +LOG_MODULE_REGISTER(lfs_backend, CONFIG_APP_STORAGE_LOG_LEVEL); + +struct storage_file_header { + uint32_t read_offset; + uint32_t write_offset; +}; + +FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(storage); +static struct fs_mount_t mountpoint = { + .type = FS_LITTLEFS, + .fs_data = &storage, + .storage_dev = (void *)FIXED_PARTITION_ID(littlefs_storage), + .mnt_point = "/storage_data", +}; + +#define RECORDS_PER_TYPE CONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPE + +static int littlefs_mount(struct fs_mount_t *mp) +{ + int rc; + + rc = fs_mount(mp); + if (rc < 0) { + LOG_PRINTK("Failed to mount id %" PRIuPTR " at %s: %d\n", + (uintptr_t)mp->storage_dev, mp->mnt_point, rc); + return rc; + } + LOG_PRINTK("%s mount: %d\n", mp->mnt_point, rc); + return 0; +} + +/** + * @brief Help function to create storage file path + * + * This function generates the file path for storing data of a specific type by concatenating + * the mount point with the type name and a .bin extension. + * file path format: "/.bin" + * + * @param type Storage data type + * @param out_path Output buffer to hold the generated file path + * @param max_len Maximum length of the output buffer + */ +static void create_storage_file_path(const struct storage_data *type, char *out_path, size_t max_len) +{ + snprintf(out_path, max_len, "%s/%s.bin", mountpoint.mnt_point, type->name); +} + +/** + * @brief Initialize storage file with header + * + * Creates a new storage file with an initialized header if it does not already exist. + * The header contains read and write offsets and the maximum number of entries. + * + * @param file_path File name (path) of the storage file + * @return int 0 on success, negative errno on failure + */ +static int init_storage_file(const char *file_path) +{ + struct fs_file_t file; + struct storage_file_header header; + struct fs_dirent entry_stat; + int rc; + + /* Check if file already exists by trying to read its status */ + rc = fs_stat(file_path, &entry_stat); + if (rc == 0) { + /* File exists, nothing to do (preserves existing header) */ + LOG_DBG("Storage file %s already exists", file_path); + return 0; + } + + /* File doesn't exist, create it and initialize header */ + fs_file_t_init(&file); + rc = fs_open(&file, file_path, FS_O_CREATE | FS_O_RDWR); + if (rc < 0) { + LOG_ERR("Failed to open %s: %d", file_path, rc); + return rc; + } + + header.read_offset = 0; + header.write_offset = 0; + + rc = fs_write(&file, &header, sizeof(header)); + if (rc < 0) { + LOG_ERR("Failed to write header %s: %d", file_path, rc); + fs_close(&file); + return rc; + } + fs_close(&file); + LOG_DBG("Initialized storage file %s", file_path); + return 0; +} + +/** + * @brief Read storage file header + * + * Reads the header of a storage file to retrieve read and write offsets and max entries. + * + * @param file Pointer to the opened storage file + * @param header Pointer to the storage file header structure to be filled + * @return int 0 on success, negative errno on failure + */ +static int read_storage_file_header(struct fs_file_t *file, struct storage_file_header *header) +{ + int rc; + + rc = fs_seek(file, 0, FS_SEEK_SET); + if (rc < 0) { + LOG_ERR("Failed to move to header: %d", rc); + return rc; + } + + rc = fs_read(file, header, sizeof(*header)); + if (rc < 0) { + LOG_ERR("Failed to read header: %d", rc); + return rc; + } + return 0; +} + +/** + * @brief Update storage file header + * + * Updates the header of a storage file with new read and write offsets. + * + * @param file Pointer to the opened storage file + * @param header Pointer to the storage file header structure to be written + * @return int 0 on success, negative errno on failure + */ +static int update_storage_file_header(struct fs_file_t *file, struct storage_file_header *header) +{ + int rc; + + rc = fs_seek(file, 0, FS_SEEK_SET); + if (rc < 0) { + LOG_ERR("Failed to move to header: %d", rc); + return rc; + } + + rc = fs_write(file, header, sizeof(*header)); + if (rc < 0) { + LOG_ERR("Failed to write header: %d", rc); + return rc; + } + return 0; +} + +/** + * @brief Initialize LittleFS storage backend + * + * Mounts the LittleFS filesystem and initializes storage files for each data type. + * + * @return int 0 on success, negative errno on failure + */ +static int lfs_storage_init(void) +{ + int num_types; + int rc; + + rc = littlefs_mount(&mountpoint); + if (rc < 0) { + LOG_ERR("LittleFS mount failed: %d", rc); + return rc; + } + + /* Ensure we don't exceed configured maximum */ + STRUCT_SECTION_COUNT(storage_data, &num_types); + __ASSERT(num_types <= CONFIG_APP_STORAGE_MAX_TYPES, + "Too many storage types registered (%d). " + "Increase CONFIG_APP_STORAGE_MAX_TYPES.", + num_types); + LOG_DBG("LittleFS storage backend mounted at %s with %d data types", + mountpoint.mnt_point, num_types); + + STRUCT_SECTION_FOREACH(storage_data, t) { + char file_path[MAX_PATH_LEN]; + + create_storage_file_path(t, file_path, sizeof(file_path)); + rc = init_storage_file(file_path); + if (rc < 0) { + LOG_ERR("Failed to initialize storage file for type %s: %d", t->name, rc); + return rc; + } + } + return 0; +} + +/** + * @brief Store data in LittleFS storage backend + * + * Stores data of a specific type into its corresponding storage file. + * If the storage file is full, the oldest data will be overwritten. + * + * @param type Type of data to store + * @param data Pointer to the data to store + * @param size Size of the data to store + * @return int 0 on success, negative errno on failure + */ +static int lfs_storage_store(const struct storage_data *type, const void *data, size_t size) +{ + char file_path[MAX_PATH_LEN]; + struct fs_file_t file; + struct storage_file_header header; + uint32_t write_pos; + int was_full; + int rc; + + if (!type || !data || size != type->data_size) { + return -EINVAL; + } + + /* Open storage file */ + create_storage_file_path(type, file_path, sizeof(file_path)); + fs_file_t_init(&file); + rc = fs_open(&file, file_path, FS_O_RDWR); + if (rc < 0) { + LOG_ERR("Failed to open %s: %d", file_path, rc); + return rc; + } + + /* Read current header */ + rc = read_storage_file_header(&file, &header); + if (rc < 0) { + fs_close(&file); + return rc; + } + + was_full = ((header.write_offset - header.read_offset) >= RECORDS_PER_TYPE); + write_pos = sizeof(header) + (header.write_offset % RECORDS_PER_TYPE) * type->data_size; + rc = fs_seek(&file, write_pos, FS_SEEK_SET); + if (rc < 0) { + LOG_ERR("Failed to move to write position: %d", rc); + fs_close(&file); + return rc; + } + + /* Write data to storage file */ + rc = fs_write(&file, data, size); + if (rc < 0) { + LOG_ERR("Failed to write data: %d", rc); + fs_close(&file); + return rc; + } + + /* Update header */ + header.write_offset++; + if (was_full) { + /* Drop oldest when full to overwrite */ + header.read_offset++; + LOG_WRN("Storage file %s full, overwriting oldest data", file_path); + } + rc = update_storage_file_header(&file, &header); + if (rc < 0) { + fs_close(&file); + return rc; + } + fs_close(&file); + return 0; +} + +/** + * @brief Peek data from LittleFS storage backend without removing it + * + * Returns the size of the next item without copying data (if data is NULL) + * or copies the data if data buffer is provided. + * + * @param type Storage data type to peek at + * @param data Pointer where the peeked data will be stored (can be NULL for size-only) + * @param size Size of the data buffer in bytes + * @return int Number of bytes that have been read (would be read if data is NULL) on success, + * -EAGAIN if no data available, negative errno on failure + */ +static int lfs_storage_peek(const struct storage_data *type, void *data, size_t size) +{ + char file_path[MAX_PATH_LEN]; + struct fs_file_t file; + struct storage_file_header header; + uint32_t read_pos; + int rc; + + /* If data is NULL, use temporary buffer to verify data exists */ + uint8_t temp_buffer[type->data_size]; + void *read_buffer = (data != NULL) ? data : temp_buffer; + + if (data != NULL && size < type->data_size) { + return -EINVAL; + } + + if (type == NULL) { + LOG_ERR("Storage type is NULL"); + return -EINVAL; + } + + /* Open storage file */ + create_storage_file_path(type, file_path, sizeof(file_path)); + fs_file_t_init(&file); + rc = fs_open(&file, file_path, FS_O_RDWR); + if (rc < 0) { + LOG_ERR("Failed to open %s: %d", file_path, rc); + return rc; + } + + /* Read current header */ + rc = read_storage_file_header(&file, &header); + if (rc < 0) { + LOG_ERR("Failed to read storage file header: %d", rc); + fs_close(&file); + return rc; + } + if (header.read_offset == header.write_offset) { + LOG_DBG("No new entries to peek in %s", file_path); + fs_close(&file); + return -EAGAIN; + } + + /* Move to read position with wrap-around */ + read_pos = sizeof(header) + (header.read_offset % RECORDS_PER_TYPE) * type->data_size; + rc = fs_seek(&file, read_pos, FS_SEEK_SET); + if (rc < 0) { + LOG_ERR("Failed to move to read position: %d", rc); + fs_close(&file); + return rc; + } + + /* Read data from storage file without advancing read offset */ + rc = fs_read(&file, read_buffer, type->data_size); + if (rc < 0) { + LOG_ERR("Failed to read data: %d", rc); + fs_close(&file); + return rc; + } + fs_close(&file); + return rc; +} + +/** + * @brief Retrieve data from LittleFS storage backend + * + * Retrieves the oldest stored data for the given data type from its storage file. + * + * @param type Storage data type to retrieve data for + * @param data Pointer where the retrieved data will be stored + * @param size Size of the data buffer in bytes + * @return int Number of bytes read on success, -EAGAIN if no data available, + * negative errno on failure + */ +static int lfs_storage_retrieve(const struct storage_data *type, void *data, size_t size) +{ + char file_path[MAX_PATH_LEN]; + struct fs_file_t file; + struct storage_file_header header; + uint32_t read_pos; + int rc, read_bytes; + + if (type == NULL || data == NULL || size < type->data_size) { + return -EINVAL; + } + + /* Open storage file */ + create_storage_file_path(type, file_path, sizeof(file_path)); + fs_file_t_init(&file); + rc = fs_open(&file, file_path, FS_O_RDWR); + if (rc < 0) { + LOG_ERR("Failed to open %s: %d", file_path, rc); + return rc; + } + /* Read current header */ + rc = read_storage_file_header(&file, &header); + if (rc < 0) { + fs_close(&file); + return rc; + } + /* Empty if counters are equal */ + if (header.read_offset == header.write_offset) { + LOG_WRN("No new entries to read in %s", file_path); + fs_close(&file); + return -EAGAIN; + } + /* Move to read position with wrap-around */ + read_pos = sizeof(header) + (header.read_offset % RECORDS_PER_TYPE) * type->data_size; + rc = fs_seek(&file, read_pos, FS_SEEK_SET); + if (rc < 0) { + LOG_ERR("Failed to move to read position: %d", rc); + fs_close(&file); + return rc; + } + /* Read data from storage file */ + read_bytes = fs_read(&file, data, type->data_size); + if (read_bytes < 0) { + LOG_ERR("Failed to read data: %d", read_bytes); + fs_close(&file); + return read_bytes; + } + /* Update header to advance read offset */ + header.read_offset++; + rc = update_storage_file_header(&file, &header); + if (rc < 0) { + fs_close(&file); + return rc; + } + fs_close(&file); + return read_bytes; +} + +/** + * @brief Get number of records of a specific type in LittleFS storage backend + * + * @param type Storage data type to count records for + * @return int Number of records on success, negative errno on failure + */ +static int lfs_storage_records_count(const struct storage_data *type) +{ + char file_path[MAX_PATH_LEN]; + struct fs_file_t file; + struct storage_file_header header; + uint32_t count; + int rc; + + if (!type) { + return -EINVAL; + } + /* Open storage file */ + create_storage_file_path(type, file_path, sizeof(file_path)); + fs_file_t_init(&file); + rc = fs_open(&file, file_path, FS_O_RDWR); + if (rc < 0) { + LOG_ERR("Failed to open %s: %d", file_path, rc); + return rc; + } + /* Read current header */ + rc = read_storage_file_header(&file, &header); + if (rc < 0) { + fs_close(&file); + return rc; + } + /* Calculate count */ + count = header.write_offset - header.read_offset; + if (count > RECORDS_PER_TYPE) { + count = RECORDS_PER_TYPE; + } + LOG_DBG("Storage file %s has %d records", file_path, count); + + fs_close(&file); + return (int)count; +} + +/** + * @brief Clear all stored data in LittleFS storage backend + * + * Resets the read and write offsets in the headers of all storage files, + * effectively clearing the stored data. + * + * @return int 0 on success, negative errno on failure + */ +static int lfs_storage_clear(void) +{ + STRUCT_SECTION_FOREACH(storage_data, type) { + char file_path[MAX_PATH_LEN]; + struct storage_file_header header; + struct fs_file_t file; + int rc; + + /* Open storage file */ + create_storage_file_path(type, file_path, sizeof(file_path)); + fs_file_t_init(&file); + rc = fs_open(&file, file_path, FS_O_RDWR); + if (rc < 0) { + LOG_ERR("Failed to open %s: %d", file_path, rc); + return rc; + } + /* Reset header offsets */ + header.read_offset = 0; + header.write_offset = 0; + rc = update_storage_file_header(&file, &header); + if (rc < 0) { + fs_close(&file); + return rc; + } + fs_close(&file); + LOG_DBG("Cleared storage file %s", file_path); + } + return 0; +} +/** + * @brief LittleFS storage backend interface + * + * Implementation of the storage_backend interface for LittleFS-based storage. + * Provides functions for initializing the backend, storing and retrieving + * data, counting stored records, and clearing all data. + */ +static const struct storage_backend lfs_backend = { + .init = lfs_storage_init, + .store = lfs_storage_store, + .peek = lfs_storage_peek, + .retrieve = lfs_storage_retrieve, + .count = lfs_storage_records_count, + .clear = lfs_storage_clear, +}; + +/** + * @brief Get the LittleFS storage backend interface + * + * Makes the LittleFS storage backend available to the storage module. + * + * @return Pointer to the LittleFS storage backend interface + */ +const struct storage_backend *storage_backend_get(void) +{ + return &lfs_backend; +} diff --git a/docs/common/architecture.md b/docs/common/architecture.md index 519eeeb7..bb331eca 100644 --- a/docs/common/architecture.md +++ b/docs/common/architecture.md @@ -292,6 +292,10 @@ SMF automatically handles the execution of exit and entry functions for all stat Modules that sample data (environmental, power, location, network) follow a consistent timestamp pattern to ensure accurate time information for cloud transmission: -- **At sampling time**: Modules capture the system uptime using `k_uptime_get()` and store it in an `int64_t uptime` field within the message structure. This represents the relative time in milliseconds when the sample was taken. +- **At sampling time**: Modules capture the system uptime using `k_uptime_get()` and attempt to convert it to Unix time. If the Unix time is valid, it is stored in a `int64_t timestamp` field within the message structure. If the Unix time is not valid (e.g., Unix time has not been synchronized yet), the module falls back to storing the timestamp as the system uptime. -- **Before cloud transmission**: The cloud module converts the uptime to Unix timestamp using `date_time_uptime_to_unix_time_ms()` immediately before sending data to the cloud. This ensures that the timestamp reflects the actual sampling time, not the transmission time. +- **Before cloud transmission**: The cloud module checks the `timestamp` field. If it contains a valid Unix timestamp (a unix time greater than 2026-01-01), it is used as is. If it contains a system uptime, the cloud module attempts to convert it to Unix timestamp using `attempt_timestamp_to_unix_ms()`, before sending data to the cloud. If this conversion also fails, the timestamp is handled according to the `CONFIG_APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS` configuration. + - `CONFIG_APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_DROP`: Samples with timestamps that cannot be converted to Unix time are dropped and not sent to the cloud. + - `CONFIG_APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_KEEP`: Samples with timestamps that cannot be converted to Unix time are kept and sent to the cloud with timestamp as is. + - `CONFIG_APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_NOW`: Samples with timestamps that cannot be converted to Unix time are assigned the current Unix time at transmission before being sent to the cloud. + - `CONFIG_APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_NO_TIMESTAMP`: Samples with timestamps that cannot be converted to Unix time are sent to the cloud with `NRF_CLOUD_NO_TIMESTAMP`, which makes nRF Cloud assign the timestamp upon reception. diff --git a/docs/common/fota.md b/docs/common/fota.md index bb4316fa..00cb4af6 100644 --- a/docs/common/fota.md +++ b/docs/common/fota.md @@ -47,7 +47,8 @@ Complete the following steps for preparing firmware: ### Version verification > [!IMPORTANT] -> The `fwversion` field in a firmware bundle is independent from the device's reported version. +> - The `fwversion` field in a firmware bundle is independent from the device's reported version. +> - **All stored data in the storage module will be automatically cleared when applying firmware updates.** If you need to preserve data across updates, ensure it is sent to the cloud or retrieved before the update completes. To verify a successful update: diff --git a/docs/modules/cloud.md b/docs/modules/cloud.md index 157edee0..807187b0 100644 --- a/docs/modules/cloud.md +++ b/docs/modules/cloud.md @@ -93,6 +93,18 @@ Several Kconfig options in `Kconfig.cloud` control this module’s behavior. The - **CONFIG_APP_CLOUD_BACKOFF_MAX_SECONDS:** Maximum reconnect backoff limit. +- **CONFIG_APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_DROP:** + Drops samples with invalid timestamps when sending data to the cloud. + +- **CONFIG_APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_KEEP:** + Sends samples with invalid timestamps to the cloud without modification. + +- **CONFIG_APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_NOW:** + Replaces invalid timestamps with the current time before sending to the cloud. + +- **CONFIG_APP_CLOUD_HANDLE_WRONG_SAMPLE_TIMESTAMPS_NO_TIMESTAMP:** + Sends samples with invalid timestamps to the cloud with `NRF_CLOUD_NO_TIMESTAMP`, which makes nRF Cloud assign the timestamp upon reception. + - **CONFIG_APP_CLOUD_THREAD_STACK_SIZE:** Stack size for the cloud module’s main thread. diff --git a/docs/modules/fota_module.md b/docs/modules/fota_module.md index fed0afbd..a908a6d5 100644 --- a/docs/modules/fota_module.md +++ b/docs/modules/fota_module.md @@ -24,6 +24,11 @@ Full modem updates require that the device disconnects from the network before a All update operations feature error handling with appropriate status messages, allowing the application to recover gracefully from download failures or interruptions. +## Storage Clearing on Reboot + +> [!IMPORTANT] +> Before rebooting to apply firmware updates, the FOTA module automatically clears all stored data in the storage module by sending a `STORAGE_CLEAR` message. This ensures a clean state after the firmware update and prevents potential data corruption or compatibility issues between firmware versions. + ## Messages The FOTA module communicates through the zbus channel `FOTA_CHAN`, using input and output messages defined in `fota.h`. diff --git a/docs/modules/storage.md b/docs/modules/storage.md index 73689b71..a28dfbc8 100644 --- a/docs/modules/storage.md +++ b/docs/modules/storage.md @@ -207,12 +207,18 @@ The following includes the key configuration categories: ### Storage Backend -- **`CONFIG_APP_STORAGE_BACKEND_RAM`** (default and only backend currently provided): Uses RAM for storage. +- **`CONFIG_APP_STORAGE_BACKEND_RAM`** (default): Uses RAM for storage. Data is lost on power cycle but provides fast access. +- **`CONFIG_APP_STORAGE_BACKEND_LITTLEFS`** : Uses the LittleFS filesystem for flash storage. + Data is persistent across power cycles, but provides slower access. + +> [!NOTE] +> Regardless of the backend used, stored data is automatically cleared when FOTA updates are applied to ensure a clean state after firmware updates. See the [FOTA module documentation](fota_module.md#storage-clearing-on-reboot) for details. + ### Memory Configuration -- **`CONFIG_APP_STORAGE_MAX_TYPES`** (default: 3): Maximum number of different data types that can be registered. +- **`CONFIG_APP_STORAGE_MAX_TYPES`** (default: 4): Maximum number of different data types that can be registered. Affects RAM usage. - **`CONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPE`** (default: 8): Maximum records stored per data type. diff --git a/tests/module/cloud/src/cloud_module_test.c b/tests/module/cloud/src/cloud_module_test.c index 2f970abb..75f19c17 100644 --- a/tests/module/cloud/src/cloud_module_test.c +++ b/tests/module/cloud/src/cloud_module_test.c @@ -112,6 +112,7 @@ FAKE_VALUE_FUNC(int, nrf_cloud_coap_agnss_data_get, FAKE_VALUE_FUNC(int, nrf_cloud_coap_location_send, const struct nrf_cloud_gnss_data *, bool); FAKE_VALUE_FUNC(int, date_time_now, int64_t *); FAKE_VALUE_FUNC(int, date_time_uptime_to_unix_time_ms, int64_t *); +FAKE_VALUE_FUNC(bool, date_time_is_valid); FAKE_VOID_FUNC(location_cloud_location_ext_result_set, enum location_ext_result, struct location_data *); FAKE_VALUE_FUNC(int, location_agnss_data_process, const char *, size_t); @@ -187,21 +188,21 @@ static int storage_batch_read_custom(struct storage_data_item *out_item, k_timeo case FAKE_BATCH_BATTERY: out_item->type = STORAGE_TYPE_BATTERY; out_item->data.BATTERY.percentage = 87.5; - out_item->data.BATTERY.uptime = TEST_BATTERY_UPTIME_MS; + out_item->data.BATTERY.timestamp = TEST_BATTERY_UPTIME_MS; break; case FAKE_BATCH_ENV: out_item->type = STORAGE_TYPE_ENVIRONMENTAL; out_item->data.ENVIRONMENTAL.temperature = 21.5; out_item->data.ENVIRONMENTAL.humidity = 40.0; out_item->data.ENVIRONMENTAL.pressure = 1002.3; - out_item->data.ENVIRONMENTAL.uptime = TEST_ENVIRONMENTAL_UPTIME_MS; + out_item->data.ENVIRONMENTAL.timestamp = TEST_ENVIRONMENTAL_UPTIME_MS; break; case FAKE_BATCH_NET: out_item->type = STORAGE_TYPE_NETWORK; out_item->data.NETWORK.type = NETWORK_QUALITY_SAMPLE_RESPONSE; out_item->data.NETWORK.conn_eval_params.energy_estimate = 5; out_item->data.NETWORK.conn_eval_params.rsrp = -96; - out_item->data.NETWORK.uptime = TEST_NETWORK_UPTIME_MS; + out_item->data.NETWORK.timestamp = TEST_NETWORK_UPTIME_MS; break; default: return -EAGAIN; @@ -397,6 +398,7 @@ void setUp(void) RESET_FAKE(nrf_cloud_coap_patch); RESET_FAKE(date_time_now); RESET_FAKE(date_time_uptime_to_unix_time_ms); + RESET_FAKE(date_time_is_valid); RESET_FAKE(nrf_provisioning_init); RESET_FAKE(nrf_provisioning_trigger_manually); RESET_FAKE(storage_batch_read); @@ -417,6 +419,7 @@ void setUp(void) /* Set default return values */ nrf_cloud_coap_location_send_fake.return_val = 0; storage_batch_read_fake.return_val = -EAGAIN; + date_time_is_valid_fake.return_val = true; /* Clear all channels */ zbus_sub_wait(&location, &chan, K_NO_WAIT); @@ -685,7 +688,7 @@ void test_gnss_location_data_handling(void) struct location_msg location_msg = { .type = LOCATION_GNSS_DATA, .gnss_data = mock_location, - .uptime = TEST_LOCATION_UPTIME_MS + .timestamp = TEST_LOCATION_UPTIME_MS }; struct storage_msg storage_data_msg = { .type = STORAGE_DATA, diff --git a/tests/module/environmental/src/environmental_module_test.c b/tests/module/environmental/src/environmental_module_test.c index 681450c1..876fdb6e 100644 --- a/tests/module/environmental/src/environmental_module_test.c +++ b/tests/module/environmental/src/environmental_module_test.c @@ -16,6 +16,7 @@ DEFINE_FFF_GLOBALS; FAKE_VALUE_FUNC(int, task_wdt_feed, int); FAKE_VALUE_FUNC(int, task_wdt_add, uint32_t, task_wdt_callback_t, void *); +FAKE_VALUE_FUNC(int, date_time_now, int64_t *); ZBUS_MSG_SUBSCRIBER_DEFINE(environmental_subscriber); ZBUS_CHAN_ADD_OBS(ENVIRONMENTAL_CHAN, environmental_subscriber, 0); @@ -27,6 +28,7 @@ void setUp(void) /* reset fakes */ RESET_FAKE(task_wdt_feed); RESET_FAKE(task_wdt_add); + RESET_FAKE(date_time_now); } void check_environmental_event(enum environmental_msg_type expected_environmental_type) diff --git a/tests/module/location/src/location_module_test.c b/tests/module/location/src/location_module_test.c index a08586d3..20c4f99f 100644 --- a/tests/module/location/src/location_module_test.c +++ b/tests/module/location/src/location_module_test.c @@ -36,6 +36,7 @@ FAKE_VALUE_FUNC(int, location_pgps_data_process, const char *, size_t); FAKE_VALUE_FUNC(int, task_wdt_feed, int); FAKE_VALUE_FUNC(int, task_wdt_add, uint32_t, task_wdt_callback_t, void *); FAKE_VALUE_FUNC(int, date_time_set, const struct tm *); +FAKE_VALUE_FUNC(int, date_time_now, int64_t *); FAKE_VALUE_FUNC(const char *, location_method_str, enum location_method); FAKE_VALUE_FUNC(int, lte_lc_func_mode_set, enum lte_lc_func_mode); @@ -307,6 +308,7 @@ void setUp(void) RESET_FAKE(task_wdt_feed); RESET_FAKE(task_wdt_add); RESET_FAKE(date_time_set); + RESET_FAKE(date_time_now); RESET_FAKE(location_method_str); /* Set up custom fakes */ diff --git a/tests/module/main/prj.conf b/tests/module/main/prj.conf index 0d012fcc..32b47ded 100644 --- a/tests/module/main/prj.conf +++ b/tests/module/main/prj.conf @@ -20,5 +20,7 @@ CONFIG_SMF=y CONFIG_SMF_ANCESTOR_SUPPORT=y CONFIG_SMF_INITIAL_TRANSITION=y CONFIG_NATIVE_SIM_SLOWDOWN_TO_REAL_TIME=n +CONFIG_DATE_TIME=y +CONFIG_DATE_TIME_NTP=n CONFIG_MAIN_STACK_SIZE=8192 diff --git a/tests/module/main/src/main.c b/tests/module/main/src/main.c index acb85166..5f0d43ae 100644 --- a/tests/module/main/src/main.c +++ b/tests/module/main/src/main.c @@ -31,7 +31,6 @@ DEFINE_FFF_GLOBALS; FAKE_VALUE_FUNC(int, dk_buttons_init, button_handler_t); FAKE_VALUE_FUNC(int, task_wdt_feed, int); FAKE_VALUE_FUNC(int, task_wdt_add, uint32_t, task_wdt_callback_t, void *); -FAKE_VOID_FUNC(date_time_register_handler, date_time_evt_handler_t); FAKE_VOID_FUNC(sys_reboot, int); LOG_MODULE_REGISTER(main_module_test, 1); @@ -106,7 +105,6 @@ void setUp(void) RESET_FAKE(dk_buttons_init); RESET_FAKE(task_wdt_feed); RESET_FAKE(task_wdt_add); - RESET_FAKE(date_time_register_handler); RESET_FAKE(sys_reboot); FFF_RESET_HISTORY(); @@ -437,6 +435,9 @@ void test_fota_waiting_for_network_disconnect(void) send_cloud_disconnected(); expect_cloud_event(CLOUD_DISCONNECTED); + /* Verify that the module sends STORAGE_CLEAR before fota reboot */ + expect_storage_event(STORAGE_CLEAR); + /* Give the system time to reboot */ k_sleep(K_SECONDS(10)); @@ -473,6 +474,9 @@ void test_fota_waiting_for_network_disconnect_to_apply_image(void) send_fota_msg(FOTA_SUCCESS_REBOOT_NEEDED); expect_fota_event(FOTA_SUCCESS_REBOOT_NEEDED); + /* Verify that the module sends STORAGE_CLEAR before fota reboot */ + expect_storage_event(STORAGE_CLEAR); + /* Give the system time to reboot */ k_sleep(K_SECONDS(10)); diff --git a/tests/module/power/src/power_module_test.c b/tests/module/power/src/power_module_test.c index f8f54933..2ba1cb5a 100644 --- a/tests/module/power/src/power_module_test.c +++ b/tests/module/power/src/power_module_test.c @@ -20,6 +20,7 @@ FAKE_VALUE_FUNC(float, nrf_fuel_gauge_process, float, float, float, float, bool, FAKE_VALUE_FUNC(int, charger_read_sensors, float *, float *, float *, int32_t *); FAKE_VALUE_FUNC(int, nrf_fuel_gauge_init, const struct nrf_fuel_gauge_init_parameters *, void *); FAKE_VALUE_FUNC(int, mfd_npm13xx_add_callback, const struct device *, struct gpio_callback *); +FAKE_VALUE_FUNC(int, date_time_now, int64_t *); ZBUS_MSG_SUBSCRIBER_DEFINE(power_subscriber); ZBUS_CHAN_ADD_OBS(POWER_CHAN, power_subscriber, 0); @@ -31,6 +32,7 @@ void setUp(void) /* reset fakes */ RESET_FAKE(task_wdt_feed); RESET_FAKE(task_wdt_add); + RESET_FAKE(date_time_now); } void check_power_event(enum power_msg_type expected_power_type) diff --git a/tests/module/storage/flash_backend/CMakeLists.txt b/tests/module/storage/flash_backend/CMakeLists.txt new file mode 100644 index 00000000..d5578ebc --- /dev/null +++ b/tests/module/storage/flash_backend/CMakeLists.txt @@ -0,0 +1,66 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(storage_module_test) + +test_runner_generate(../src/storage_module_test.c) + +target_sources(app + PRIVATE + ../src/storage_module_test.c + ../src/test_samples.c + ../../../../app/src/modules/storage/storage.c + ../../../../app/src/modules/storage/storage_data_types.c + ../../../../app/src/modules/storage/backends/littlefs_backend.c +) + +zephyr_include_directories(${ZEPHYR_BASE}/include/zephyr/) +zephyr_include_directories(${ZEPHYR_BASE}/subsys/testsuite/include) +zephyr_include_directories(../../../../app/src/modules/storage) +zephyr_include_directories(../../../../app/src/modules/storage/backends) +zephyr_include_directories(../../../../app/src/common) +zephyr_include_directories(../../../../app/src/modules/power) +zephyr_include_directories(../../../../app/src/modules/network) +zephyr_include_directories(../../../../app/src/modules/environmental) +zephyr_include_directories(../../../../app/src/modules/location) +zephyr_include_directories(${ZEPHYR_BASE}/../nrfxlib/nrf_modem/include) +zephyr_include_directories(../src) + +zephyr_linker_sources(SECTIONS ../../../../app/src/modules/storage/storage_sections.ld) + +# The test uses double precision floating point numbers. This is not enabled by default in unity +# unless we set the following define. +zephyr_compile_definitions(UNITY_INCLUDE_DOUBLE) + +# Options that cannot be passed through Kconfig fragments +target_compile_definitions(app PRIVATE + -DCONFIG_LOG=1 + -DCONFIG_APP_STORAGE_LOG_LEVEL=4 + -DCONFIG_APP_STORAGE_INITIAL_MODE_BUFFER=1 + -DCONFIG_APP_STORAGE_MAX_TYPES=4 + -DCONFIG_APP_STORAGE_MAX_RECORDS_PER_TYPE=100 + -DCONFIG_APP_STORAGE_MESSAGE_QUEUE_SIZE=10 + -DCONFIG_APP_STORAGE_THREAD_STACK_SIZE=2048 + -DCONFIG_APP_STORAGE_WATCHDOG_TIMEOUT_SECONDS=120 + -DCONFIG_APP_STORAGE_MSG_PROCESSING_TIMEOUT_SECONDS=60 + -DCONFIG_APP_POWER=1 + -DCONFIG_APP_ENVIRONMENTAL=1 + -DCONFIG_APP_STORAGE_BATCH_BUFFER_SIZE=1024 + -DCONFIG_APP_STORAGE_SESSION_TIMEOUT_SECONDS=60 + -DCONFIG_APP_LOCATION=1 + -DCONFIG_APP_LOCATION_WIFI_APS_MAX=10 + -DCONFIG_APP_LOCATION_NEIGHBOR_CELLS_MAX=10 + -DCONFIG_LTE_NEIGHBOR_CELLS_MAX=10 + -DCONFIG_LOCATION_METHOD_WIFI_SCANNING_RESULTS_MAX_CNT=10 + -DCONFIG_LOCATION_SERVICE_EXTERNAL + -DCONFIG_LOCATION_METHOD_CELLULAR=1 + -DCONFIG_LOCATION_METHOD_WIFI=1 + -DCONFIG_LOCATION_METHODS_LIST_SIZE=3 + -DCONFIG_LTE_LC_CONN_EVAL_MODULE=1 +) diff --git a/tests/module/storage/flash_backend/boards/native_sim.overlay b/tests/module/storage/flash_backend/boards/native_sim.overlay new file mode 100644 index 00000000..c38415df --- /dev/null +++ b/tests/module/storage/flash_backend/boards/native_sim.overlay @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019 Jan Van Winkel + * + * SPDX-License-Identifier: Apache-2.0 + */ + +&flashcontroller0 { + reg = <0x00000000 DT_SIZE_K(4096)>; +}; + +&flash0 { + reg = <0x00000000 DT_SIZE_K(4096)>; + partitions { + compatible = "fixed-partitions"; + #address-cells = <1>; + #size-cells = <1>; + + littlefs_storage: partition@0 { + label = "littlefs_storage"; + reg = <0x00000000 0x00100000>; + }; + }; +}; diff --git a/tests/module/storage/flash_backend/boards/native_sim_native_64.overlay b/tests/module/storage/flash_backend/boards/native_sim_native_64.overlay new file mode 100644 index 00000000..e4994493 --- /dev/null +++ b/tests/module/storage/flash_backend/boards/native_sim_native_64.overlay @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2024 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "native_sim.overlay" diff --git a/tests/module/storage/flash_backend/prj.conf b/tests/module/storage/flash_backend/prj.conf new file mode 100644 index 00000000..5d5f7bae --- /dev/null +++ b/tests/module/storage/flash_backend/prj.conf @@ -0,0 +1,28 @@ +# +# Copyright (c) 2025 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +CONFIG_UNITY=y +CONFIG_ZBUS=y +CONFIG_LOG=y +CONFIG_LOG_MODE_IMMEDIATE=y +CONFIG_ZBUS_OBSERVER_NAME=y +CONFIG_ZBUS_CHANNEL_NAME=y +CONFIG_ZBUS_MSG_SUBSCRIBER=y +CONFIG_ZBUS_RUNTIME_OBSERVERS=y +CONFIG_CBPRINTF_FP_SUPPORT=y +CONFIG_HEAP_MEM_POOL_SIZE=80000 + +CONFIG_SMF=y +CONFIG_SMF_ANCESTOR_SUPPORT=y +CONFIG_SMF_INITIAL_TRANSITION=y + +CONFIG_NATIVE_SIM_SLOWDOWN_TO_REAL_TIME=n + +CONFIG_FLASH=y +CONFIG_FLASH_MAP=y + +CONFIG_FILE_SYSTEM=y +CONFIG_FILE_SYSTEM_LITTLEFS=y diff --git a/tests/module/storage/flash_backend/testcase.yaml b/tests/module/storage/flash_backend/testcase.yaml new file mode 100644 index 00000000..ced6ab75 --- /dev/null +++ b/tests/module/storage/flash_backend/testcase.yaml @@ -0,0 +1,8 @@ +tests: + storage.module.flash: + tags: storage + platform_allow: + - native_sim + - native_sim/native/64 + integration_platforms: + - native_sim diff --git a/tests/module/storage/CMakeLists.txt b/tests/module/storage/ram_backend/CMakeLists.txt similarity index 65% rename from tests/module/storage/CMakeLists.txt rename to tests/module/storage/ram_backend/CMakeLists.txt index d21635ab..2d1798e8 100644 --- a/tests/module/storage/CMakeLists.txt +++ b/tests/module/storage/ram_backend/CMakeLists.txt @@ -9,30 +9,30 @@ cmake_minimum_required(VERSION 3.20.0) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(storage_module_test) -test_runner_generate(src/storage_module_test.c) +test_runner_generate(../src/storage_module_test.c) target_sources(app PRIVATE - src/storage_module_test.c - src/test_samples.c - ../../../app/src/modules/storage/storage.c - ../../../app/src/modules/storage/storage_data_types.c - ../../../app/src/modules/storage/backends/ram_ring_buffer_backend.c + ../src/storage_module_test.c + ../src/test_samples.c + ../../../../app/src/modules/storage/storage.c + ../../../../app/src/modules/storage/storage_data_types.c + ../../../../app/src/modules/storage/backends/ram_ring_buffer_backend.c ) zephyr_include_directories(${ZEPHYR_BASE}/include/zephyr/) zephyr_include_directories(${ZEPHYR_BASE}/subsys/testsuite/include) -zephyr_include_directories(../../../app/src/modules/storage) -zephyr_include_directories(../../../app/src/modules/storage/backends) -zephyr_include_directories(../../../app/src/common) -zephyr_include_directories(../../../app/src/modules/power) -zephyr_include_directories(../../../app/src/modules/network) -zephyr_include_directories(../../../app/src/modules/environmental) -zephyr_include_directories(../../../app/src/modules/location) +zephyr_include_directories(../../../../app/src/modules/storage) +zephyr_include_directories(../../../../app/src/modules/storage/backends) +zephyr_include_directories(../../../../app/src/common) +zephyr_include_directories(../../../../app/src/modules/power) +zephyr_include_directories(../../../../app/src/modules/network) +zephyr_include_directories(../../../../app/src/modules/environmental) +zephyr_include_directories(../../../../app/src/modules/location) zephyr_include_directories(${ZEPHYR_BASE}/../nrfxlib/nrf_modem/include) -zephyr_include_directories(src) +zephyr_include_directories(../src) -zephyr_linker_sources(SECTIONS ../../../app/src/modules/storage/storage_sections.ld) +zephyr_linker_sources(SECTIONS ../../../../app/src/modules/storage/storage_sections.ld) # The test uses double precision floating point numbers. This is not enabled by default in unity # unless we set the following define. diff --git a/tests/module/storage/prj.conf b/tests/module/storage/ram_backend/prj.conf similarity index 100% rename from tests/module/storage/prj.conf rename to tests/module/storage/ram_backend/prj.conf diff --git a/tests/module/storage/testcase.yaml b/tests/module/storage/ram_backend/testcase.yaml similarity index 86% rename from tests/module/storage/testcase.yaml rename to tests/module/storage/ram_backend/testcase.yaml index 17fcb323..839cf284 100644 --- a/tests/module/storage/testcase.yaml +++ b/tests/module/storage/ram_backend/testcase.yaml @@ -1,5 +1,5 @@ tests: - storage.module: + storage.module.ram: tags: storage platform_allow: - native_sim