From 22dc23dbe92b549a404c7e9efb63d74e6d2d6e7d Mon Sep 17 00:00:00 2001 From: Tommi Rantanen Date: Mon, 2 Feb 2026 07:08:48 +0200 Subject: [PATCH 1/2] app: nrfcloud: Add GCI support to cellpos Implement cell positioning similarly to Location library meaning AT%NCELLMEAS will be done 1-3 times depending on how many cells are requested and found in each attempt. Implementation done by copying GCI parsing from lte_lc. Signed-off-by: Tommi Rantanen --- app/Kconfig | 4 + app/prj.conf | 1 - app/src/sm_at_gnss.c | 66 +- app/src/sm_at_nrfcloud.c | 1303 +++++++++++++---- app/src/sm_at_nrfcloud.h | 43 +- doc/app/at_nrfcloud.rst | 99 +- .../migration_notes_ncs_slm_v3.1.x.rst | 18 + doc/releases/migration_notes_v2.0.0.rst | 17 +- 8 files changed, 1182 insertions(+), 369 deletions(-) diff --git a/app/Kconfig b/app/Kconfig index 19e83d07..a8100fcc 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -248,6 +248,9 @@ config SM_GNSS config SM_NRF_CLOUD bool "nRF Cloud support" default y + depends on NRF_CLOUD + depends on NRF_CLOUD_COAP + depends on DATE_TIME select EXPERIMENTAL if SM_NRF_CLOUD @@ -261,6 +264,7 @@ endif # SM_NRF_CLOUD config SM_NRF_CLOUD_LOCATION bool "nRF Cloud Location support" + default y depends on SM_NRF_CLOUD help Enable the nRF Cloud Location service for cloud-assisted geolocation. diff --git a/app/prj.conf b/app/prj.conf index 88312426..ec0cb87d 100644 --- a/app/prj.conf +++ b/app/prj.conf @@ -95,7 +95,6 @@ CONFIG_SETTINGS_NVS=y CONFIG_NVS=y # nRF Cloud -CONFIG_SM_NRF_CLOUD_LOCATION=y CONFIG_NRF_CLOUD=y CONFIG_NRF_CLOUD_COAP=y CONFIG_NRF_CLOUD_COAP_DOWNLOADS=n diff --git a/app/src/sm_at_gnss.c b/app/src/sm_at_gnss.c index cd9f76aa..da25bd0c 100644 --- a/app/src/sm_at_gnss.c +++ b/app/src/sm_at_gnss.c @@ -59,7 +59,13 @@ enum sm_gnss_operation { #if defined(CONFIG_NRF_CLOUD_AGNSS) static struct k_work agnss_req_work; -#endif + +#if defined(CONFIG_SM_NRF_CLOUD_LOCATION) +static K_SEM_DEFINE(agnss_ncellmeas_sem, 0, 1); +static struct lte_lc_cells_info *agnss_net_info; +#endif /* CONFIG_SM_NRF_CLOUD_LOCATION */ +#endif /* CONFIG_SM_NRF_CLOUD */ + #if defined(CONFIG_NRF_CLOUD_PGPS) static struct k_work pgps_req_work; static struct k_work pgps_coap_req_work; @@ -317,13 +323,26 @@ static int read_agnss_req(struct nrf_modem_gnss_agnss_data_frame *req) #endif /* CONFIG_NRF_CLOUD_AGNSS || CONFIG_NRF_CLOUD_PGPS */ #if defined(CONFIG_NRF_CLOUD_AGNSS) + +#if defined(CONFIG_SM_NRF_CLOUD_LOCATION) +static void agnss_ncellmeas_done(struct lte_lc_cells_info *cell_data, void *ctx) +{ + ARG_UNUSED(ctx); + agnss_net_info = cell_data; + k_sem_give(&agnss_ncellmeas_sem); +} +#endif /* CONFIG_SM_NRF_CLOUD_LOCATION */ + static void agnss_requestor(struct k_work *) { int err; struct nrf_modem_gnss_agnss_data_frame req; struct nrf_cloud_coap_agnss_request request = { - NRF_CLOUD_COAP_AGNSS_REQ_CUSTOM, - &req, + .type = NRF_CLOUD_COAP_AGNSS_REQ_CUSTOM, + .agnss_req = &req, + .net_info = NULL, + .filtered = false, + .mask_angle = 0, }; char *agnss_rest_data_buf = calloc(1, NRF_CLOUD_AGNSS_MAX_DATA_SIZE); @@ -339,20 +358,43 @@ static void agnss_requestor(struct k_work *) } struct nrf_cloud_coap_agnss_result result = { - agnss_rest_data_buf, - NRF_CLOUD_AGNSS_MAX_DATA_SIZE, - 0 + .buf = agnss_rest_data_buf, + .buf_sz = NRF_CLOUD_AGNSS_MAX_DATA_SIZE, + .agnss_sz = 0, }; - struct lte_lc_cells_info net_info = {0}; - err = get_single_cell_info(&net_info.current_cell); - if (err) { - LOG_ERR("Failed to obtain single-cell cellular network information (%d).", err); - goto cleanup; +#if defined(CONFIG_SM_NRF_CLOUD_LOCATION) + /* Start async ncellmeas and wait for the result via semaphore. + * agnss_ncellmeas_done() is called from the AT monitor thread + * (not sm_work_q) for the single-phase case, so giving the semaphore + * does not deadlock this work item. + */ + struct lte_lc_cells_info *net_info = NULL; + + err = sm_at_nrfcloud_ncellmeas_start(1, false, agnss_ncellmeas_done, NULL); + if (err == 0) { + /* We will wait for 5 seconds for NCELLMEAS to complete. */ + k_sem_take(&agnss_ncellmeas_sem, K_SECONDS(5)); + net_info = agnss_net_info; + agnss_net_info = NULL; } - request.net_info = &net_info; + if (net_info != NULL && net_info->current_cell.id != LTE_LC_CELL_EUTRAN_ID_INVALID) { + request.net_info = net_info; + } else { + LOG_WRN("Requesting A-GNSS data without location assistance"); + sm_at_nrfcloud_ncellmeas_cleanup(net_info); + net_info = NULL; + } +#else + LOG_INF("Requesting A-GNSS data without location assistance " + "since CONFIG_SM_NRF_CLOUD_LOCATION is not defined"); +#endif err = nrf_cloud_coap_agnss_data_get(&request, &result); +#if defined(CONFIG_SM_NRF_CLOUD_LOCATION) + sm_at_nrfcloud_ncellmeas_cleanup(net_info); + net_info = NULL; +#endif if (err) { LOG_ERR("Failed to request A-GNSS data via CoAP (%d).", err); goto cleanup; diff --git a/app/src/sm_at_nrfcloud.c b/app/src/sm_at_nrfcloud.c index 73db0a78..cec32550 100644 --- a/app/src/sm_at_nrfcloud.c +++ b/app/src/sm_at_nrfcloud.c @@ -3,7 +3,6 @@ * * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause */ -#if defined(CONFIG_SM_NRF_CLOUD) #include #include @@ -16,7 +15,6 @@ #include "nrf_cloud_coap_transport.h" #include #include -#include #include "sm_util.h" #include "sm_at_host.h" #include "sm_at_nrfcloud.h" @@ -24,291 +22,88 @@ LOG_MODULE_REGISTER(sm_nrfcloud, CONFIG_SM_LOG_LEVEL); static K_SEM_DEFINE(sem_date_time, 0, 1); -uint8_t sm_at_buf[CONFIG_SM_AT_BUF_SIZE + 1]; static struct modem_pipe *nrfcloud_pipe; -#if defined(CONFIG_SM_NRF_CLOUD_LOCATION) - -/* Cellular positioning services .*/ -enum sm_nrfcloud_cellpos { - CELLPOS_NONE = 0, - CELLPOS_SINGLE_CELL, - CELLPOS_MULTI_CELL, - - CELLPOS_COUNT /* Keep last. */ -}; -/* Whether cellular positioning is requested and if so, which. */ -static enum sm_nrfcloud_cellpos nrfcloud_cell_pos = CELLPOS_NONE; - -/* Whether Wi-Fi positioning is requested. */ -static bool nrfcloud_wifi_pos; - -/* Whether a location request is currently being sent to nRF Cloud. */ -static bool nrfcloud_sending_loc_req; - -static struct k_work nrfcloud_loc_req; -static struct k_work nrfcloud_conn_work; +static char nrfcloud_device_id[NRF_CLOUD_CLIENT_ID_MAX_LEN]; +bool sm_nrf_cloud_ready; +bool sm_nrf_cloud_send_location; /* Parameters saved before submitting connection work. */ static bool nrfcloud_connect; static bool nrfcloud_conn_send_location; -/** Definitions for %NCELLMEAS notification - * %NCELLMEAS: status [,, , , , , - * , , ,,] - * [,1, 1, 1, 1,1] - * [,2, 2, 2, 2,2] ... - * [,17, 17, 17, 17,17 - * - * Max 17 ncell - * 11 number of parameters for current cell (including "%NCELLMEAS") - * 5 number of parameters for one neighboring cell - */ -#define MAX_PARAM_CELL 11 -#define MAX_PARAM_NCELL 5 -/* Neighbor cells are not very useful for positioning so we don't need to support 10 or 17 */ -#define NCELL_CNT 5 - -/* Neighboring cell measurements. */ -static struct lte_lc_ncell nrfcloud_ncells[NCELL_CNT]; - -/* nRF Cloud location request cellular data. */ -static struct lte_lc_cells_info nrfcloud_cell_data = { - .neighbor_cells = nrfcloud_ncells, - .gci_cells_count = 0 -}; -static bool nrfcloud_ncellmeas_done; - -#define WIFI_APS_BEGIN_IDX 3 - -/* nRF Cloud location request Wi-Fi data. */ -static struct wifi_scan_info nrfcloud_wifi_data; - -#endif /* CONFIG_SM_NRF_CLOUD_LOCATION */ - -static char nrfcloud_device_id[NRF_CLOUD_CLIENT_ID_MAX_LEN]; - -bool sm_nrf_cloud_ready; -bool sm_nrf_cloud_send_location; +static void nrfcloud_conn_work_fn(struct k_work *work); +K_WORK_DEFINE(nrfcloud_conn_work, nrfcloud_conn_work_fn); #if defined(CONFIG_SM_NRF_CLOUD_LOCATION) -AT_MONITOR(ncell_meas, "NCELLMEAS", ncell_meas_mon, PAUSED); - -static void ncell_meas_mon(const char *notify) -{ - int err; - uint32_t param_count; - int ncellmeas_status; - char cid[9] = {0}; - size_t size; - char plmn[6] = {0}; - char mcc[4] = {0}; - char tac[9] = {0}; - unsigned int ncells_count = 0; - struct at_parser parser; - nrfcloud_ncellmeas_done = false; - err = at_parser_init(&parser, notify); - if (err) { - goto exit; - } - - /* parse status, 0: success 1: fail */ - err = at_parser_num_get(&parser, 1, &ncellmeas_status); - if (err) { - goto exit; - } - if (ncellmeas_status != 0) { - LOG_ERR("NCELLMEAS failed"); - err = -EAGAIN; - goto exit; - } - - err = at_parser_cmd_count_get(&parser, ¶m_count); - if (err) { - goto exit; - } - if (param_count < MAX_PARAM_CELL) { /* at least current cell */ - LOG_ERR("Missing param in NCELLMEAS notification"); - err = -EAGAIN; - goto exit; - } +#define NRFCLOUDPOS_TIMEOUT_SEC 120 - /* parse Cell ID */ - size = sizeof(cid); - err = util_string_get(&parser, 2, cid, &size); - if (err) { - goto exit; - } - err = util_str_to_int(cid, 16, (int *)&nrfcloud_cell_data.current_cell.id); - if (err) { - goto exit; - } +/* NCELLMEAS notification parameters */ +#define AT_NCELLMEAS_STATUS_INDEX 1 +#define AT_NCELLMEAS_CELL_ID_INDEX 2 +#define AT_NCELLMEAS_PLMN_INDEX 3 +#define AT_NCELLMEAS_TAC_INDEX 4 +#define AT_NCELLMEAS_TIMING_ADV_INDEX 5 +#define AT_NCELLMEAS_EARFCN_INDEX 6 +#define AT_NCELLMEAS_PHYS_CELL_ID_INDEX 7 +#define AT_NCELLMEAS_RSRP_INDEX 8 +#define AT_NCELLMEAS_RSRQ_INDEX 9 +#define AT_NCELLMEAS_MEASUREMENT_TIME_INDEX 10 - /* parse PLMN */ - size = sizeof(plmn); - err = util_string_get(&parser, 3, plmn, &size); - if (err) { - goto exit; - } - strncpy(mcc, plmn, 3); /* MCC always 3-digit */ - err = util_str_to_int(mcc, 10, &nrfcloud_cell_data.current_cell.mcc); - if (err) { - goto exit; - } - err = util_str_to_int(&plmn[3], 10, &nrfcloud_cell_data.current_cell.mnc); - if (err) { - goto exit; - } +#define AT_NCELLMEAS_PRE_NCELLS_PARAMS_COUNT 11 +#define AT_NCELLMEAS_NCELLS_PARAMS_COUNT 5 +#define AT_NCELLMEAS_GCI_CELL_PARAMS_COUNT 12 - /* parse TAC */ - size = sizeof(tac); - err = util_string_get(&parser, 4, tac, &size); - if (err) { - goto exit; - } - err = util_str_to_int(tac, 16, (int *)&nrfcloud_cell_data.current_cell.tac); - if (err) { - goto exit; - } +#define AT_NCELLMEAS_STATUS_VALUE_FAIL 1 +#define AT_NCELLMEAS_STATUS_VALUE_INCOMPLETE 2 - /* omit timing_advance */ - nrfcloud_cell_data.current_cell.timing_advance = NRF_CLOUD_LOCATION_CELL_OMIT_TIME_ADV; - - /* parse EARFCN */ - err = at_parser_num_get(&parser, 6, &nrfcloud_cell_data.current_cell.earfcn); - if (err) { - goto exit; - } - - /* parse PCI */ - err = at_parser_num_get(&parser, 7, - &nrfcloud_cell_data.current_cell.phys_cell_id); - if (err) { - goto exit; - } - - /* parse RSRP and RSRQ */ - err = at_parser_num_get(&parser, 8, &nrfcloud_cell_data.current_cell.rsrp); - if (err < 0) { - goto exit; - } - err = at_parser_num_get(&parser, 9, &nrfcloud_cell_data.current_cell.rsrq); - if (err < 0) { - goto exit; - } - - /* omit measurement_time and parse neighboring cells */ - - ncells_count = (param_count - MAX_PARAM_CELL) / MAX_PARAM_NCELL; - for (unsigned int i = 0; i != ncells_count; ++i) { - const unsigned int offset = MAX_PARAM_CELL + i * MAX_PARAM_NCELL; - - if (i >= NCELL_CNT) { - LOG_INF("Too many neighboring cells (%d) for allocated buffer (%d)", - i, NCELL_CNT); - break; - } - - /* parse n_earfcn */ - err = at_parser_num_get(&parser, offset, &nrfcloud_ncells[i].earfcn); - if (err < 0) { - goto exit; - } - - /* parse n_phys_cell_id */ - err = at_parser_num_get(&parser, offset + 1, - &nrfcloud_ncells[i].phys_cell_id); - if (err < 0) { - goto exit; - } - - /* parse n_rsrp */ - err = at_parser_num_get(&parser, offset + 2, &nrfcloud_ncells[i].rsrp); - if (err < 0) { - goto exit; - } +/* Whether cellular positioning is requested and if so, which. */ +static uint8_t nrfcloud_cell_count; - /* parse n_rsrq */ - err = at_parser_num_get(&parser, offset + 3, &nrfcloud_ncells[i].rsrq); - if (err < 0) { - goto exit; - } - /* omit time_diff */ - } +/* Whether Wi-Fi positioning is requested. */ +static bool nrfcloud_wifi_pos; - err = 0; - nrfcloud_cell_data.ncells_count = ncells_count; - nrfcloud_ncellmeas_done = true; +/* Whether a location request is currently being sent to nRF Cloud. */ +static bool nrfcloud_sending_loc_req; -exit: - LOG_INF("NCELLMEAS notification parse (err: %d)", err); -} +static uint8_t ncellmeas_search_type; +static uint8_t ncellmeas_gci_count; -int get_single_cell_info(struct lte_lc_cell *const cell_inf) -{ - int err; - struct modem_param_info modem_inf; +/* ---------- NCELLMEAS async state machine ---------- */ - err = modem_info_params_init(&modem_inf); - if (err) { - LOG_ERR("Could not initialize modem info module, error: %d", err); - return err; - } - err = modem_info_params_get(&modem_inf); - if (err) { - LOG_ERR("Could not obtain information from modem, error: %d", err); - return err; - } - cell_inf->mcc = modem_inf.network.mcc.value; - cell_inf->mnc = modem_inf.network.mnc.value; - cell_inf->tac = modem_inf.network.area_code.value; - cell_inf->id = modem_inf.network.cellid_dec; - cell_inf->rsrp = modem_inf.network.rsrp.value; +enum ncellmeas_state { + NCELLMEAS_STATE_IDLE, + NCELLMEAS_STATE_FIRST_WAIT, /* AT%NCELLMEAS=1 issued, awaiting URC */ + NCELLMEAS_STATE_RRC_IDLE_WAIT, /* Polling AT+CSCON? for RRC idle */ + NCELLMEAS_STATE_SECOND_WAIT, /* AT%NCELLMEAS=3 issued, awaiting URC */ + NCELLMEAS_STATE_THIRD_WAIT, /* AT%NCELLMEAS=4 issued, awaiting URC */ +}; - return 0; -} +static enum ncellmeas_state ncellmeas_sm_state = NCELLMEAS_STATE_IDLE; +static uint8_t ncellmeas_req_cell_count; +static bool ncellmeas_send_loc_req_flag; +static sm_at_nrfcloud_ncellmeas_done_cb_t ncellmeas_done_cb; +static void *ncellmeas_done_ctx; +static uint8_t ncellmeas_rrc_polls; -static void loc_req_wk(struct k_work *work) -{ - int err = 0; +static void nrfcloud_loc_req_work_fn(struct k_work *work); +static void sm_at_nrfcloud_ncellmeas_work_fn(struct k_work *work); +static void ncellmeas_state_handle_work_fn(struct k_work *work); +static void ncellmeas_timeout_work_fn(struct k_work *work); - if (nrfcloud_cell_pos == CELLPOS_SINGLE_CELL) { - LOG_DBG("Requesting single-cell location from nRF Cloud."); - /* Obtain the single cell info from the modem */ - err = get_single_cell_info(&nrfcloud_cell_data.current_cell); - if (err) { - LOG_ERR("Failed to obtain single-cell cellular network information (%d).", - err); - } else { - nrfcloud_cell_data.ncells_count = 0; - /* Invalidate the last neighboring cell measurements - * because they have been partly overwritten. - */ - nrfcloud_ncellmeas_done = false; - } - } +K_WORK_DEFINE(nrfcloud_loc_req_work, nrfcloud_loc_req_work_fn); +K_WORK_DEFINE(sm_at_nrfcloud_ncellmeas_work, sm_at_nrfcloud_ncellmeas_work_fn); +K_WORK_DELAYABLE_DEFINE(ncellmeas_state_handle_work, ncellmeas_state_handle_work_fn); +K_WORK_DELAYABLE_DEFINE(ncellmeas_timeout_backup_work, ncellmeas_timeout_work_fn); - struct nrf_cloud_location_result result = {0}; - const struct nrf_cloud_coap_location_request request = { - .config = NULL, - .cell_info = nrfcloud_cell_pos ? &nrfcloud_cell_data : NULL, - .wifi_info = nrfcloud_wifi_pos ? &nrfcloud_wifi_data : NULL}; +/* nRF Cloud location request cellular data. */ +static struct lte_lc_cells_info *nrfcloud_cell_data; - err = nrf_cloud_coap_location_get(&request, &result); - if (err == 0) { - urc_send_to(nrfcloud_pipe, "\r\n#XNRFCLOUDPOS: %d,%lf,%lf,%d\r\n", - result.type, result.lat, result.lon, result.unc); - } else { - LOG_ERR("Failed to request nRF Cloud location (%d).", err); - urc_send_to(nrfcloud_pipe, "\r\n#XNRFCLOUDPOS: %d\r\n", err < 0 ? -1 : err); - } +#define WIFI_APS_BEGIN_IDX 3 - if (nrfcloud_wifi_pos) { - k_free(nrfcloud_wifi_data.ap_info); - } - nrfcloud_sending_loc_req = false; -} +/* nRF Cloud location request Wi-Fi data. */ +static struct wifi_scan_info nrfcloud_wifi_data; #endif /* CONFIG_SM_NRF_CLOUD_LOCATION */ @@ -331,9 +126,6 @@ static void on_cloud_ready(void) sm_nrf_cloud_ready = true; urc_send_to(nrfcloud_pipe, "\r\n#XNRFCLOUD: %d,%d\r\n", sm_nrf_cloud_ready, sm_nrf_cloud_send_location); -#if defined(CONFIG_SM_NRF_CLOUD_LOCATION) - at_monitor_resume(&ncell_meas); -#endif } static void on_cloud_disconnected(void) @@ -341,9 +133,6 @@ static void on_cloud_disconnected(void) sm_nrf_cloud_ready = false; urc_send_to(nrfcloud_pipe, "\r\n#XNRFCLOUD: %d,%d\r\n", sm_nrf_cloud_ready, sm_nrf_cloud_send_location); -#if defined(CONFIG_SM_NRF_CLOUD_LOCATION) - at_monitor_pause(&ncell_meas); -#endif } static void date_time_event_handler(const struct date_time_evt *evt) @@ -388,7 +177,7 @@ static int nrf_cloud_datamode_callback(uint8_t op, const uint8_t *data, int len, return 0; } -static void conn_wk(struct k_work *work) +static void nrfcloud_conn_work_fn(struct k_work *work) { int err; @@ -422,7 +211,7 @@ static void conn_wk(struct k_work *work) } SM_AT_CMD_CUSTOM(xnrfcloud, "AT#XNRFCLOUD", handle_at_nrf_cloud); -static int handle_at_nrf_cloud(enum at_parser_cmd_type cmd_type, struct at_parser *parser, +STATIC int handle_at_nrf_cloud(enum at_parser_cmd_type cmd_type, struct at_parser *parser, uint32_t param_count) { enum sm_nrfcloud_operation { @@ -464,7 +253,7 @@ static int handle_at_nrf_cloud(enum at_parser_cmd_type cmd_type, struct at_parse k_work_submit_to_queue(&sm_work_q, &nrfcloud_conn_work); err = 0; } else { - err = -EINVAL; + err = -EBUSY; } break; case AT_PARSER_CMD_TYPE_READ: { @@ -486,14 +275,44 @@ static int handle_at_nrf_cloud(enum at_parser_cmd_type cmd_type, struct at_parse return err; } +static void sm_at_nrfcloud_init(int ret, void *ctx) +{ + static bool initialized; + int err; + + if (initialized) { + return; + } + + err = nrf_cloud_coap_init(); + if (err) { + LOG_ERR("Failed to initialize nRF Cloud CoAP library: %d", err); + return; + } + + err = nrf_cloud_client_id_get(nrfcloud_device_id, sizeof(nrfcloud_device_id)); + if (err) { + LOG_ERR("Failed to get nRF Cloud client ID: %d", err); + return; + } + + initialized = true; +} +NRF_MODEM_LIB_ON_INIT(sm_nrfcloud_init_hook, sm_at_nrfcloud_init, NULL); + +/****************************************************************/ +/* Cellular positioning, i.e., %NCELLMEAS notification handling */ +/****************************************************************/ + #if defined(CONFIG_SM_NRF_CLOUD_LOCATION) SM_AT_CMD_CUSTOM(xnrfcloudpos, "AT#XNRFCLOUDPOS", handle_at_nrf_cloud_pos); -static int handle_at_nrf_cloud_pos(enum at_parser_cmd_type cmd_type, +STATIC int handle_at_nrf_cloud_pos(enum at_parser_cmd_type cmd_type, struct at_parser *parser, uint32_t param_count) { int err; - uint16_t cell_pos, wifi_pos; + uint16_t cell_count = 0; + uint16_t wifi_pos = 0; if (cmd_type != AT_PARSER_CMD_TYPE_SET) { return -ENOTSUP; @@ -514,7 +333,7 @@ static int handle_at_nrf_cloud_pos(enum at_parser_cmd_type cmd_type, return -EINVAL; } - err = at_parser_num_get(parser, 1, &cell_pos); + err = at_parser_num_get(parser, 1, &cell_count); if (err) { return err; } @@ -524,27 +343,27 @@ static int handle_at_nrf_cloud_pos(enum at_parser_cmd_type cmd_type, return err; } - if (cell_pos >= CELLPOS_COUNT || wifi_pos > 1) { + if (cell_count > 15 || wifi_pos > 1) { return -EINVAL; } - if (!cell_pos && !wifi_pos) { + if (!cell_count && !wifi_pos) { LOG_ERR("At least one of cellular/Wi-Fi information must be included."); return -EINVAL; } - if (cell_pos == CELLPOS_MULTI_CELL && !nrfcloud_ncellmeas_done) { - LOG_ERR("%s", "No neighboring cell measurement. Did you run `AT%NCELLMEAS`?"); - return -EAGAIN; - } - if (!wifi_pos && param_count > WIFI_APS_BEGIN_IDX) { /* No Wi-Fi AP allowed if no Wi-Fi positioning. */ return -E2BIG; } + if (wifi_pos && param_count < WIFI_APS_BEGIN_IDX + 1) { + /* Wi-Fi AP required if Wi-Fi positioning. */ + return -ENOENT; + } + if (wifi_pos) { - nrfcloud_wifi_data.ap_info = k_malloc( + nrfcloud_wifi_data.ap_info = malloc( sizeof(*nrfcloud_wifi_data.ap_info) * (param_count - WIFI_APS_BEGIN_IDX)); if (!nrfcloud_wifi_data.ap_info) { return -ENOMEM; @@ -609,43 +428,929 @@ static int handle_at_nrf_cloud_pos(enum at_parser_cmd_type cmd_type, nrfcloud_wifi_data.cnt, NRF_CLOUD_LOCATION_WIFI_AP_CNT_MIN); } if (err) { - k_free(nrfcloud_wifi_data.ap_info); + free(nrfcloud_wifi_data.ap_info); return err; } } - nrfcloud_cell_pos = cell_pos; + nrfcloud_cell_count = cell_count; nrfcloud_wifi_pos = wifi_pos; nrfcloud_sending_loc_req = true; - k_work_submit_to_queue(&sm_work_q, &nrfcloud_loc_req); + if (cell_count >= 1) { + /* To workqueue to be able to send OK response */ + k_work_submit_to_queue(&sm_work_q, &sm_at_nrfcloud_ncellmeas_work); + } else { + k_work_submit_to_queue(&sm_work_q, &nrfcloud_loc_req_work); + } return 0; } -#endif /* CONFIG_SM_NRF_CLOUD_LOCATION */ +/* This function has been copied from sdk-nrf nrf/lib/lte_link_control library + * with version v3.4.0-rc1. + */ +static int string_to_int(const char *str_buf, int base, int *output) +{ + int temp; + char *end_ptr; -static void sm_at_nrfcloud_init(int ret, void *ctx) + __ASSERT_NO_MSG(str_buf != NULL); + + errno = 0; + temp = strtol(str_buf, &end_ptr, base); + + if (end_ptr == str_buf || *end_ptr != '\0' || + ((temp == LONG_MAX || temp == LONG_MIN) && errno == ERANGE)) { + return -ENODATA; + } + + *output = temp; + + return 0; +} + +/* This function has been copied from sdk-nrf lte_link_control library + * with version v3.4.0-rc1. + */ +static int string_param_to_int(struct at_parser *parser, size_t idx, int *output, int base) { - static bool initialized; int err; + char str_buf[16]; + size_t len = sizeof(str_buf) - 1; - if (initialized) { + __ASSERT_NO_MSG(parser != NULL); + __ASSERT_NO_MSG(output != NULL); + + err = at_parser_string_get(parser, idx, str_buf, &len); + if (err) { + return err; + } + str_buf[len] = '\0'; + + if (string_to_int(str_buf, base, output)) { + return -ENODATA; + } + + return 0; +} + +/* This function has been copied from sdk-nrf lte_link_control library + * with version v3.4.0-rc1. + */ +static int plmn_param_string_to_mcc_mnc(struct at_parser *parser, size_t idx, int *mcc, int *mnc) +{ + int err; + char str_buf[7]; + size_t len = sizeof(str_buf); + + err = at_parser_string_get(parser, idx, str_buf, &len); + if (err) { + LOG_ERR("Could not get PLMN, error: %d", err); + return err; + } + + str_buf[len] = '\0'; + + /* Read MNC and store as integer. The MNC starts as the fourth character + * in the string, following three characters long MCC. + */ + err = string_to_int(&str_buf[3], 10, mnc); + if (err) { + LOG_ERR("Could not get MNC, error: %d", err); + return err; + } + + /* NUL-terminate MCC, read and store it. */ + str_buf[3] = '\0'; + + err = string_to_int(str_buf, 10, mcc); + if (err) { + LOG_ERR("Could not get MCC, error: %d", err); + return err; + } + + return 0; +} + +AT_MONITOR(sm_ncellmeas, "NCELLMEAS", at_handler_ncellmeas, PAUSED); + +void sm_at_nrfcloud_ncellmeas_cleanup(struct lte_lc_cells_info *cell_data) +{ + if (cell_data == NULL) { return; } - initialized = true; - err = nrf_cloud_coap_init(); + free(cell_data->neighbor_cells); + cell_data->neighbor_cells = NULL; + free(cell_data->gci_cells); + cell_data->gci_cells = NULL; + free(cell_data); +} + +/* Finalise a measurement run; must be called from sm_work_q context. */ +static void ncellmeas_complete(void) +{ + k_work_cancel_delayable(&ncellmeas_timeout_backup_work); + /* Cancel the RRC poll if it is pending. */ + k_work_cancel_delayable(&ncellmeas_state_handle_work); + at_monitor_pause(&sm_ncellmeas); + ncellmeas_sm_state = NCELLMEAS_STATE_IDLE; + + if (ncellmeas_send_loc_req_flag) { + k_work_submit_to_queue(&sm_work_q, &nrfcloud_loc_req_work); + } else { + /* Transfer cell_data ownership to the callback / caller. */ + struct lte_lc_cells_info *cell_data = nrfcloud_cell_data; + + nrfcloud_cell_data = NULL; + nrfcloud_sending_loc_req = false; + if (ncellmeas_done_cb) { + ncellmeas_done_cb(cell_data, ncellmeas_done_ctx); + } else { + sm_at_nrfcloud_ncellmeas_cleanup(cell_data); + } + } +} + +/* This function has been adapted from sdk-nrf location library + * scan_cellular_execute() with version v3.4.0-rc1. + * The blocking semaphore waits and k_sleep() RRC poll loop have been removed + * in favour of the URC-driven state machine. + */ +int sm_at_nrfcloud_ncellmeas_start(uint8_t cell_count, bool send_loc_req, + sm_at_nrfcloud_ncellmeas_done_cb_t cb, void *ctx) +{ + int err; + struct lte_lc_cells_info *cell_data; + + /* Allocate main cell data structure. + * Neighbor/GCI cells are only allocated when they are found. + */ + cell_data = calloc(1, sizeof(struct lte_lc_cells_info)); + if (cell_data == NULL) { + LOG_ERR("Failed to allocate memory for the nRF Cloud cell data"); + if (send_loc_req) { + k_work_submit_to_queue(&sm_work_q, &nrfcloud_loc_req_work); + } + return -ENOMEM; + } + + nrfcloud_sending_loc_req = true; + cell_data->current_cell.id = LTE_LC_CELL_EUTRAN_ID_INVALID; + cell_data->ncells_count = 0; + cell_data->gci_cells_count = 0; + /* Set global so that URC parsing callbacks can access it. */ + nrfcloud_cell_data = cell_data; + + ncellmeas_req_cell_count = cell_count; + ncellmeas_send_loc_req_flag = send_loc_req; + ncellmeas_done_cb = cb; + ncellmeas_done_ctx = ctx; + + /* Schedule backup timeout on sm_work_q so it serialises with the rest + * of the state machine and cannot race with ncellmeas_complete(). + */ + k_work_schedule_for_queue(&sm_work_q, &ncellmeas_timeout_backup_work, + K_SECONDS(NRFCLOUDPOS_TIMEOUT_SEC)); + at_monitor_resume(&sm_ncellmeas); + + LOG_DBG("Triggering cell measurements cell_count=%d", cell_count); + + /***** + * 1st: Normal neighbor search to get current cell. + * In addition neighbor cells are received. + */ + LOG_DBG("Normal neighbor search (NCELLMEAS=1)"); + ncellmeas_search_type = 1; + ncellmeas_sm_state = NCELLMEAS_STATE_FIRST_WAIT; + err = sm_util_at_printf("AT%%NCELLMEAS=1"); if (err) { - LOG_ERR("Failed to initialize nRF Cloud CoAP library: %d", err); + LOG_ERR("NCELLMEAS failed: %d", err); + ncellmeas_complete(); + return err; + } + + /* The rest of the flow is driven by %NCELLMEAS URCs via + * at_handler_ncellmeas() → ncellmeas_state_handle_work_fn(). + */ + return 0; +} + +/* Advance the state machine after %NCELLMEAS URC has been parsed. + * Runs on sm_work_q, submitted by at_handler_ncellmeas(). + */ +static void ncellmeas_state_handle_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + + int err; + int rrc_mode; + uint8_t ncellmeas_2nd_cell_count; + struct lte_lc_cell *cells; + + switch (ncellmeas_sm_state) { + case NCELLMEAS_STATE_FIRST_WAIT: + if (ncellmeas_req_cell_count <= 1) { + ncellmeas_complete(); + return; + } + /* GCI searches require RRC idle; start polling. */ + ncellmeas_rrc_polls = 0; + ncellmeas_sm_state = NCELLMEAS_STATE_RRC_IDLE_WAIT; + k_work_schedule_for_queue(&sm_work_q, &ncellmeas_state_handle_work, K_NO_WAIT); + break; + + case NCELLMEAS_STATE_RRC_IDLE_WAIT: + /* GCI searches are not done when in RRC connected mode. We poll for the + * RRC connection release every second for up to 10 seconds. + * We could subscribe to CSCON notifications but that would require + * coordinating SM vs. host subscriptions. + */ + err = sm_util_at_scanf("AT+CSCON?", "+CSCON: %*d,%d", &rrc_mode); + if (err == 1 && rrc_mode != 0 && ncellmeas_rrc_polls < 10) { + LOG_DBG("Waiting for RRC connection release (%d/10)", + ncellmeas_rrc_polls + 1); + ncellmeas_rrc_polls++; + k_work_schedule_for_queue( + &sm_work_q, + &ncellmeas_state_handle_work, + K_SECONDS(1)); + return; + } + + if (err != 1) { + /* If AT+CSCON fails, proceed anyway with GCI searches. */ + LOG_ERR("+CSCON failed %d, proceeding with GCI search", err); + } + + /***** + * 2nd: GCI history search to get GCI cells we can quickly search and measure. + * Because history search is quick and very power efficient, we request + * minimum of 5 cells even if less has been requested. + */ + ncellmeas_2nd_cell_count = MAX(5, ncellmeas_req_cell_count); + LOG_DBG("GCI history search (NCELLMEAS=3,%d)", ncellmeas_2nd_cell_count); + + cells = calloc(ncellmeas_2nd_cell_count, sizeof(struct lte_lc_cell)); + if (cells == NULL) { + LOG_ERR("Failed to allocate memory for the GCI cells"); + ncellmeas_complete(); + return; + } + nrfcloud_cell_data->gci_cells = cells; + + ncellmeas_search_type = 3; + ncellmeas_gci_count = ncellmeas_2nd_cell_count; + ncellmeas_sm_state = NCELLMEAS_STATE_SECOND_WAIT; + err = sm_util_at_printf("AT%%NCELLMEAS=3,%d", ncellmeas_2nd_cell_count); + if (err) { + LOG_WRN("NCELLMEAS=3 failed: %d, using data from phase 1", err); + ncellmeas_complete(); + } + break; + + case NCELLMEAS_STATE_SECOND_WAIT: + if (nrfcloud_cell_data->gci_cells_count + 1 >= ncellmeas_req_cell_count) { + ncellmeas_complete(); + return; + } + /***** + * 3rd: GCI regional search to try and get requested number of GCI cells. + * This search can be time and power consuming especially in rural areas + * depending on the available bands in the region. + */ + LOG_DBG("GCI regional search (NCELLMEAS=4,%d)", ncellmeas_req_cell_count); + ncellmeas_search_type = 4; + ncellmeas_gci_count = ncellmeas_req_cell_count; + ncellmeas_sm_state = NCELLMEAS_STATE_THIRD_WAIT; + err = sm_util_at_printf("AT%%NCELLMEAS=4,%d", ncellmeas_req_cell_count); + if (err) { + LOG_WRN("NCELLMEAS=4 failed: %d, using data from earlier phases", err); + ncellmeas_complete(); + } + break; + + case NCELLMEAS_STATE_THIRD_WAIT: + ncellmeas_complete(); + break; + + default: + LOG_WRN("URC received in unexpected NCELLMEAS state %d", ncellmeas_sm_state); + break; + } +} + +static void ncellmeas_timeout_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + + LOG_WRN("NCELLMEAS timeout"); + ncellmeas_complete(); +} + +static void sm_at_nrfcloud_ncellmeas_work_fn(struct k_work *work) +{ + ARG_UNUSED(work); + + sm_at_nrfcloud_ncellmeas_start(nrfcloud_cell_count, true, NULL, NULL); +} + +static void nrfcloud_loc_req_work_fn(struct k_work *work) +{ + int err = 0; + + struct nrf_cloud_location_result result = {0}; + struct nrf_cloud_coap_location_request request = { + .config = NULL, + .cell_info = nrfcloud_cell_count ? nrfcloud_cell_data : NULL, + .wifi_info = nrfcloud_wifi_pos ? &nrfcloud_wifi_data : NULL + }; + + /* Check if ncellmeas was requested but there are no results */ + if (nrfcloud_cell_count > 0) { + if (nrfcloud_cell_data == NULL || + nrfcloud_cell_data->current_cell.id == LTE_LC_CELL_EUTRAN_ID_INVALID) { + request.cell_info = NULL; + if (nrfcloud_wifi_pos) { + LOG_WRN("NCELLMEAS failed but Wi-Fi APs are available"); + /* Continue to send Wi-Fi APs to cloud*/ + } else { + urc_send_to(nrfcloud_pipe, "\r\n#XNRFCLOUDPOS: -1\r\n"); + goto clean_exit; + } + } + } + + err = nrf_cloud_coap_location_get(&request, &result); + if (err == 0) { + urc_send_to(nrfcloud_pipe, "\r\n#XNRFCLOUDPOS: 0,%d,%lf,%lf,%d\r\n", + result.type, result.lat, result.lon, result.unc); + } else { + LOG_ERR("Failed to request nRF Cloud location (%d).", err); + urc_send_to(nrfcloud_pipe, "\r\n#XNRFCLOUDPOS: %d\r\n", err < 0 ? -1 : err); + } + +clean_exit: + sm_at_nrfcloud_ncellmeas_cleanup(nrfcloud_cell_data); + nrfcloud_cell_data = NULL; + if (nrfcloud_wifi_pos) { + free(nrfcloud_wifi_data.ap_info); + nrfcloud_wifi_data.ap_info = NULL; + } + nrfcloud_sending_loc_req = false; +} + +/* Counts the frequency of a character in a null-terminated string. */ +static uint32_t get_char_frequency(const char *str, char c) +{ + uint32_t count = 0; + + __ASSERT_NO_MSG(str != NULL); + + do { + if (*str == c) { + count++; + } + } while (*(str++) != '\0'); + + return count; +} + +static uint32_t neighborcell_count_get(const char *at_response) +{ + uint32_t comma_count, ncell_elements, ncell_count; + + __ASSERT_NO_MSG(at_response != NULL); + + comma_count = get_char_frequency(at_response, ','); + if (comma_count < AT_NCELLMEAS_PRE_NCELLS_PARAMS_COUNT) { + return 0; + } + + /* Add one, as there's no comma after the last element. */ + ncell_elements = comma_count - (AT_NCELLMEAS_PRE_NCELLS_PARAMS_COUNT - 1) + 1; + ncell_count = ncell_elements / AT_NCELLMEAS_NCELLS_PARAMS_COUNT; + + return ncell_count; +} + +static struct lte_lc_ncell *parse_ncellmeas_neighbors( + struct at_parser *parser, + uint8_t ncell_count, + int *curr_index) +{ + int err; + struct lte_lc_ncell *ncells = NULL; + int tmp_int = 0; + size_t j = 0; + + if (ncell_count == 0) { + return NULL; + } + /* Allocate room for the parsed neighbor info. */ + ncells = calloc(ncell_count, sizeof(struct lte_lc_ncell)); + if (ncells == NULL) { + LOG_ERR("OOM: ncells"); + return NULL; + } + + /* Parse neighbors */ + for (j = 0; j < ncell_count; j++) { + /* */ + err = at_parser_num_get(parser, *curr_index, &ncells[j].earfcn); + if (err) { + LOG_ERR("Could not parse n_earfcn, error: %d", err); + goto error_exit; + } + + /* */ + (*curr_index)++; + err = at_parser_num_get(parser, *curr_index, &ncells[j].phys_cell_id); + if (err) { + LOG_ERR("Could not parse n_phys_cell_id, error: %d", err); + goto error_exit; + } + + /* */ + (*curr_index)++; + err = at_parser_num_get(parser, *curr_index, &tmp_int); + if (err) { + LOG_ERR("Could not parse n_rsrp, error: %d", err); + goto error_exit; + } + ncells[j].rsrp = tmp_int; + + /* */ + (*curr_index)++; + err = at_parser_num_get(parser, *curr_index, &tmp_int); + if (err) { + LOG_ERR("Could not parse n_rsrq, error: %d", err); + goto error_exit; + } + ncells[j].rsrq = tmp_int; + + /* */ + (*curr_index)++; + err = at_parser_num_get(parser, *curr_index, &ncells[j].time_diff); + if (err) { + LOG_ERR("Could not parse time_diff, error: %d", err); + goto error_exit; + } + (*curr_index)++; + } + + return ncells; + +error_exit: + free(ncells); + return NULL; +} + +/* This function has been copied from sdk-nrf lte_link_control library + * with version v3.4.0-rc1. + */ +static int parse_ncellmeas_gci(const char *at_response, struct lte_lc_cells_info *cells) +{ + struct at_parser parser; + int err, status, tmp_int, len; + int16_t tmp_short; + char tmp_str[7]; + int curr_index; + size_t i = 0, k = 0; + + __ASSERT_NO_MSG(at_response != NULL); + __ASSERT_NO_MSG(cells != NULL); + __ASSERT_NO_MSG(cells->gci_cells != NULL); + + /* Count the actual number of parameters in the AT response before + * allocating heap for it. This may save quite a bit of heap as the + * worst case scenario is 96 elements. + * 3 is added to account for the parameters that do not have a trailing + * comma. + */ + size_t param_count = get_char_frequency(at_response, ',') + 3; + + /* We don't want to clear old current cell since it's not always returned but + * we want to overwrite old GCI cells. + */ + cells->gci_cells_count = 0; + + /* + * Response format for GCI search types: + * High level: + * status[, + * GCI_cell_info1,neighbor_count1[,neighbor_cell1_1,neighbor_cell1_2...], + * GCI_cell_info2,neighbor_count2[,neighbor_cell2_1,neighbor_cell2_2...]...] + * + * Detailed: + * %NCELLMEAS: status + * [,,,,,,,,,, + * ,, + * [,,,,,] + * [,,,,,]...], + * ,,,,,,,,, + * ,, + * [,,,,,] + * [,,,,,]...]... + */ + + err = at_parser_init(&parser, at_response); + __ASSERT_NO_MSG(err == 0); + + /* Status code */ + curr_index = AT_NCELLMEAS_STATUS_INDEX; + err = at_parser_num_get(&parser, curr_index, &status); + if (err) { + LOG_DBG("Cannot parse NCELLMEAS status"); + goto clean_exit; + } + + if (status == AT_NCELLMEAS_STATUS_VALUE_FAIL) { + err = 1; + LOG_WRN("NCELLMEAS failed"); + goto clean_exit; + } else if (status == AT_NCELLMEAS_STATUS_VALUE_INCOMPLETE) { + LOG_WRN("NCELLMEAS interrupted; results incomplete"); + if (param_count == 3) { + /* No results, skip parsing. */ + goto clean_exit; + } + } + + /* Go through the cells */ + for (i = 0; curr_index < (param_count - (AT_NCELLMEAS_GCI_CELL_PARAMS_COUNT + 1)) && + i < ncellmeas_gci_count; + i++) { + struct lte_lc_cell parsed_cell; + bool is_serving_cell; + uint8_t parsed_ncells_count; + + /* */ + curr_index++; + err = string_param_to_int(&parser, curr_index, &tmp_int, 16); + if (err) { + LOG_ERR("Could not parse cell_id, index %d, i %d error: %d", curr_index, i, + err); + goto clean_exit; + } + + if (tmp_int > LTE_LC_CELL_EUTRAN_ID_MAX) { + LOG_WRN("cell_id = %d which is > LTE_LC_CELL_EUTRAN_ID_MAX; " + "marking invalid", + tmp_int); + tmp_int = LTE_LC_CELL_EUTRAN_ID_INVALID; + } + parsed_cell.id = tmp_int; + + /* */ + len = sizeof(tmp_str); + + curr_index++; + err = at_parser_string_get(&parser, curr_index, tmp_str, &len); + if (err) { + LOG_ERR("Could not parse plmn, error: %d", err); + goto clean_exit; + } + + /* Read MNC and store as integer. The MNC starts as the fourth character + * in the string, following three characters long MCC. + */ + err = string_to_int(&tmp_str[3], 10, &parsed_cell.mnc); + if (err) { + LOG_ERR("string_to_int, error: %d", err); + goto clean_exit; + } + + /* Null-terminated MCC, read and store it. */ + tmp_str[3] = '\0'; + + err = string_to_int(tmp_str, 10, &parsed_cell.mcc); + if (err) { + LOG_ERR("string_to_int, error: %d", err); + goto clean_exit; + } + + /* */ + curr_index++; + err = string_param_to_int(&parser, curr_index, &tmp_int, 16); + if (err) { + LOG_ERR("Could not parse tracking_area_code in i %d, error: %d", i, err); + goto clean_exit; + } + parsed_cell.tac = tmp_int; + + /* */ + curr_index++; + err = at_parser_num_get(&parser, curr_index, &tmp_int); + if (err) { + LOG_ERR("Could not parse timing_advance, error: %d", err); + goto clean_exit; + } + parsed_cell.timing_advance = tmp_int; + + /* */ + curr_index++; + err = at_parser_num_get(&parser, curr_index, &parsed_cell.timing_advance_meas_time); + if (err) { + LOG_ERR("Could not parse timing_advance_meas_time, error: %d", err); + goto clean_exit; + } + + /* */ + curr_index++; + err = at_parser_num_get(&parser, curr_index, &parsed_cell.earfcn); + if (err) { + LOG_ERR("Could not parse earfcn, error: %d", err); + goto clean_exit; + } + + /* */ + curr_index++; + err = at_parser_num_get(&parser, curr_index, &parsed_cell.phys_cell_id); + if (err) { + LOG_ERR("Could not parse phys_cell_id, error: %d", err); + goto clean_exit; + } + + /* */ + curr_index++; + err = at_parser_num_get(&parser, curr_index, &parsed_cell.rsrp); + if (err) { + LOG_ERR("Could not parse rsrp, error: %d", err); + goto clean_exit; + } + + /* */ + curr_index++; + err = at_parser_num_get(&parser, curr_index, &parsed_cell.rsrq); + if (err) { + LOG_ERR("Could not parse rsrq, error: %d", err); + goto clean_exit; + } + + /* */ + curr_index++; + err = at_parser_num_get(&parser, curr_index, &parsed_cell.measurement_time); + if (err) { + LOG_ERR("Could not parse meas_time, error: %d", err); + goto clean_exit; + } + + /* */ + curr_index++; + err = at_parser_num_get(&parser, curr_index, &tmp_short); + if (err) { + LOG_ERR("Could not parse serving, error: %d", err); + goto clean_exit; + } + is_serving_cell = tmp_short; + + /* */ + curr_index++; + err = at_parser_num_get(&parser, curr_index, &tmp_short); + if (err) { + LOG_ERR("Could not parse neighbor_count, error: %d", err); + goto clean_exit; + } + parsed_ncells_count = tmp_short; + + if (is_serving_cell) { + /* This the current/serving cell. + * In practice the is always 0 for other than + * the serving cell, i.e. no neigbor cell list is available. + * Thus, handle neighbor cells only for the serving cell. + */ + cells->current_cell = parsed_cell; + if (parsed_ncells_count != 0) { + /* Free any neighbor_cells allocated by a prior parse pass + * (e.g. from NCELLMEAS=1) before overwriting the pointer. + */ + free(cells->neighbor_cells); + cells->neighbor_cells = NULL; + cells->ncells_count = 0; + cells->neighbor_cells = parse_ncellmeas_neighbors( + &parser, parsed_ncells_count, &curr_index); + if (cells->neighbor_cells == NULL) { + LOG_ERR("Failed to parse neighbor cells"); + err = -EFAULT; + goto clean_exit; + } + cells->ncells_count = parsed_ncells_count; + } + } else { + cells->gci_cells[k] = parsed_cell; + cells->gci_cells_count++; /* Increase count for non-serving GCI cell */ + k++; + } + } + +clean_exit: + return err; +} + +/* This function has been copied from sdk-nrf lte_link_control library + * with version v3.4.0-rc1. + */ +static int parse_ncellmeas(const char *at_response, struct lte_lc_cells_info *cells) +{ + int err, status, tmp; + struct at_parser parser; + size_t count = 0; + int ncells_start_idx = AT_NCELLMEAS_PRE_NCELLS_PARAMS_COUNT; + + __ASSERT_NO_MSG(at_response != NULL); + __ASSERT_NO_MSG(cells != NULL); + + err = at_parser_init(&parser, at_response); + __ASSERT_NO_MSG(err == 0); + + /** + * Definitions for %NCELLMEAS notification + * %NCELLMEAS: status [,, , , , , + * , , ,,] + * [,1, 1, 1, 1,1] + * [,2, 2, 2, 2,2] ... + * [,17, 17, 17, 17,17 + * + * Max 17 ncell + */ + + err = at_parser_cmd_count_get(&parser, &count); + if (err) { + LOG_ERR("Could not get NCELLMEAS param count, " + "potentially malformed notification, error: %d", + err); + goto clean_exit; + } + + /* Status code */ + err = at_parser_num_get(&parser, AT_NCELLMEAS_STATUS_INDEX, &status); + if (err) { + goto clean_exit; + } + + if (status == AT_NCELLMEAS_STATUS_VALUE_FAIL) { + err = 1; + LOG_WRN("NCELLMEAS failed"); + goto clean_exit; + } else if (status == AT_NCELLMEAS_STATUS_VALUE_INCOMPLETE) { + LOG_WRN("NCELLMEAS interrupted; results incomplete"); + if (count == 2) { + /* No results, skip parsing. */ + goto clean_exit; + } + } + + /* Current cell ID */ + err = string_param_to_int(&parser, AT_NCELLMEAS_CELL_ID_INDEX, &tmp, 16); + if (err) { + goto clean_exit; + } + + if (tmp > LTE_LC_CELL_EUTRAN_ID_MAX) { + tmp = LTE_LC_CELL_EUTRAN_ID_INVALID; + } + cells->current_cell.id = tmp; + + /* PLMN, that is, MCC and MNC */ + err = plmn_param_string_to_mcc_mnc(&parser, AT_NCELLMEAS_PLMN_INDEX, + &cells->current_cell.mcc, &cells->current_cell.mnc); + if (err) { + goto clean_exit; + } + + /* Tracking area code */ + err = string_param_to_int(&parser, AT_NCELLMEAS_TAC_INDEX, &tmp, 16); + if (err) { + goto clean_exit; + } + + cells->current_cell.tac = tmp; + + /* Timing advance */ + err = at_parser_num_get(&parser, AT_NCELLMEAS_TIMING_ADV_INDEX, &tmp); + if (err) { + goto clean_exit; + } + + cells->current_cell.timing_advance = tmp; + + /* EARFCN */ + err = at_parser_num_get(&parser, AT_NCELLMEAS_EARFCN_INDEX, &cells->current_cell.earfcn); + if (err) { + goto clean_exit; + } + + /* Physical cell ID */ + err = at_parser_num_get(&parser, AT_NCELLMEAS_PHYS_CELL_ID_INDEX, + &cells->current_cell.phys_cell_id); + if (err) { + goto clean_exit; + } + + /* RSRP */ + err = at_parser_num_get(&parser, AT_NCELLMEAS_RSRP_INDEX, &tmp); + if (err) { + goto clean_exit; + } + + cells->current_cell.rsrp = tmp; + + /* RSRQ */ + err = at_parser_num_get(&parser, AT_NCELLMEAS_RSRQ_INDEX, &tmp); + if (err) { + goto clean_exit; + } + + cells->current_cell.rsrq = tmp; + + /* Measurement time */ + err = at_parser_num_get(&parser, AT_NCELLMEAS_MEASUREMENT_TIME_INDEX, + &cells->current_cell.measurement_time); + if (err) { + goto clean_exit; + } + + /* Neighbor cell count */ + cells->ncells_count = neighborcell_count_get(at_response); + + /* Timing advance measurement time is added as the last parameter in the response. */ + size_t ta_meas_time_index = AT_NCELLMEAS_PRE_NCELLS_PARAMS_COUNT + + cells->ncells_count * AT_NCELLMEAS_NCELLS_PARAMS_COUNT; + + if (count > ta_meas_time_index) { + err = at_parser_num_get(&parser, ta_meas_time_index, + &cells->current_cell.timing_advance_meas_time); + if (err) { + goto clean_exit; + } + } else { + cells->current_cell.timing_advance_meas_time = 0; + } + + if (cells->ncells_count == 0) { + goto clean_exit; + } + + cells->neighbor_cells = parse_ncellmeas_neighbors( + &parser, cells->ncells_count, &ncells_start_idx); + if (cells->neighbor_cells == NULL) { + LOG_ERR("Failed to parse neighbor cells"); + err = -EFAULT; + } + +clean_exit: + return err; +} + +/* This function has been copied from sdk-nrf lte_link_control library + * with version v3.4.0-rc1. + * The structure has been modified from lte_link_control library as follows: + * - parse_ncellmeas_gci is called in this handler and not from parse_ncellmeas() function. + * - New parse_ncellmeas_neighbors() function handles neighbors parsing for + * both GCI and non-GCI search types. + */ +static void at_handler_ncellmeas(const char *response) +{ + int err; + + __ASSERT_NO_MSG(response != NULL); + __ASSERT_NO_MSG(nrfcloud_sending_loc_req); + __ASSERT_NO_MSG(nrfcloud_cell_data != NULL); + + if (ncellmeas_search_type > 2) { + err = parse_ncellmeas_gci(response, nrfcloud_cell_data); + } else { + err = parse_ncellmeas(response, nrfcloud_cell_data); + } + + switch (err) { + case 0: + LOG_DBG("NCELLMEAS parsed successfully, err: %d, gci_count: %d, ncells_count: %d", + err, nrfcloud_cell_data->gci_cells_count, nrfcloud_cell_data->ncells_count); + break; + /* case 1: NCELLMEAS failed */ + default: + LOG_ERR("NCELLMEAS parsing failed, err: %d", err); + break; + } + + if (err) { + ncellmeas_complete(); return; } - k_work_init(&nrfcloud_conn_work, conn_wk); -#if defined(CONFIG_SM_NRF_CLOUD_LOCATION) - k_work_init(&nrfcloud_loc_req, loc_req_wk); -#endif - nrf_cloud_client_id_get(nrfcloud_device_id, sizeof(nrfcloud_device_id)); + /* For single-phase callback callers (e.g. A-GNSS, cell_count=1), complete + * directly on this thread (AT monitor) so the callback can safely give a + * semaphore without deadlocking sm_work_q. All other paths (multi-phase + * or loc-req) are handled via sm_work_q as usual. + */ + if (ncellmeas_done_cb != NULL && ncellmeas_req_cell_count <= 1 && + ncellmeas_sm_state == NCELLMEAS_STATE_FIRST_WAIT) { + ncellmeas_complete(); + } else { + k_work_schedule_for_queue(&sm_work_q, &ncellmeas_state_handle_work, K_NO_WAIT); + } } -NRF_MODEM_LIB_ON_INIT(sm_nrfcloud_init_hook, sm_at_nrfcloud_init, NULL); -#endif /* CONFIG_SM_NRF_CLOUD */ +#endif /* CONFIG_SM_NRF_CLOUD_LOCATION */ diff --git a/app/src/sm_at_nrfcloud.h b/app/src/sm_at_nrfcloud.h index 167cd903..ff947491 100644 --- a/app/src/sm_at_nrfcloud.h +++ b/app/src/sm_at_nrfcloud.h @@ -13,6 +13,7 @@ * @{ */ #include +#include /* Whether the connection to nRF Cloud is ready. */ extern bool sm_nrf_cloud_ready; @@ -21,13 +22,43 @@ extern bool sm_nrf_cloud_ready; extern bool sm_nrf_cloud_send_location; /** - * @brief Get information about the current cell. - * @param[out] cell_inf Cell information structure to be filled. + * @brief Callback invoked on completion of an async NCELLMEAS run. * - * @retval 0 If the operation was successful. - * Otherwise, a (negative) error code is returned. + * Called from sm_work_q context. The callback takes ownership of @p cell_data + * and must release it with sm_at_nrfcloud_ncellmeas_cleanup() when done. + * @p cell_data may be NULL if allocation failed. + * + * @param[in] cell_data Measurement results, or NULL on allocation failure. + * @param[in] ctx Opaque pointer passed to sm_at_nrfcloud_ncellmeas_start(). */ -int get_single_cell_info(struct lte_lc_cell *const cell_inf); +typedef void (*sm_at_nrfcloud_ncellmeas_done_cb_t)(struct lte_lc_cells_info *cell_data, void *ctx); + +/** + * @brief Start asynchronous cellular neighbor cell measurements. + * + * Issues the first AT%%NCELLMEAS command and returns immediately. + * Progress is URC-driven; @p cb is invoked from sm_work_q when all phases + * are complete. Pass @p cb = NULL with @p send_loc_req = true to have the + * implementation submit the nRF Cloud location request automatically. + * + * @param[in] cell_count Number of cells to search for. + * @param[in] send_loc_req If true, submit the nRF Cloud location request on + * completion (cb is ignored). + * @param[in] cb Completion callback (used when send_loc_req is false). + * @param[in] ctx Opaque pointer forwarded to @p cb. + * + * @return 0 on success, negative errno on error. + */ +int sm_at_nrfcloud_ncellmeas_start(uint8_t cell_count, bool send_loc_req, + sm_at_nrfcloud_ncellmeas_done_cb_t cb, void *ctx); + +/** + * @brief Cleanup the cellular neighbor cell measurement data. + * + * @param[in] cell_data Data to be cleaned up. + */ +void sm_at_nrfcloud_ncellmeas_cleanup(struct lte_lc_cells_info *cell_data); -/** @} */ #endif /* SM_AT_NRFCLOUD_ */ + +/** @} */ diff --git a/doc/app/at_nrfcloud.rst b/doc/app/at_nrfcloud.rst index a811b413..49672fde 100644 --- a/doc/app/at_nrfcloud.rst +++ b/doc/app/at_nrfcloud.rst @@ -44,25 +44,25 @@ Syntax AT#XNRFCLOUD=[,] -The ```` parameter can have the following integer values: +* The ```` parameter can have the following integer values: -* ``0`` - Disconnect from the nRF Cloud service. -* ``1`` - Connect to the nRF Cloud service. -* ``2`` - Send a message in the JSON format to the nRF Cloud service. + * ``0`` - Disconnect from the nRF Cloud service. + * ``1`` - Connect to the nRF Cloud service. + * ``2`` - Send a message in the JSON format to the nRF Cloud service. -When ```` is ``2``, |SM| enters :ref:`sm_data_mode`. + When ```` is ``2``, |SM| enters :ref:`sm_data_mode`. -The ```` parameter is used only when the value of ```` is ``1``. -It can have the following integer values: +* The ```` parameter is used only when the value of ```` is ``1``. + It can have the following integer values: -* ``0`` - The device location is not sent to nRF Cloud. - This is the default behavior if the parameter is omitted. -* ``1`` - The device location is sent to nRF Cloud. + * ``0`` - The device location is not sent to nRF Cloud. + This is the default behavior if the parameter is omitted. + * ``1`` - The device location is sent to nRF Cloud. -.. note:: - The location is sent to the nRF Cloud whenever a fix is produced by the GNSS module. - You must use the :ref:`#XGNSS ` AT command to start GNSS either in single-fix or periodic navigation mode. - The interval between fixes must be at least 5 seconds. + .. note:: + The location is sent to the nRF Cloud whenever a fix is produced by the GNSS module. + You must use the :ref:`#XGNSS ` AT command to start GNSS either in single-fix or periodic navigation mode. + The interval between fixes must be at least 5 seconds. Unsolicited notification ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -192,50 +192,47 @@ Syntax :: - AT#XNRFCLOUDPOS=,[,[,],[,][,[...]]] + AT#XNRFCLOUDPOS=,[,[,],[,][,[...]]] -The ```` parameter can have the following integer values: +* The ```` parameter indicates the number of cells to include in the location request. + The value range is ``0`` to ``15``. + For cellular positioning, a recommended value is ``4``. + ``0`` means that no cellular network information will be included in the location request. + The |SM| uses the ``AT%NCELLMEAS`` command to retrieve the cellular network information, and depending on the value of ````, the command might be executed multiple times. -* ``0`` - Do not include cellular network information in the location request. -* ``1`` - Use single-cell cellular network information (only the serving cell). -* ``2`` - Use multi-cell cellular network information (the serving and possibly neighboring cells). - To use this option, you must first issue the ``AT%NCELLMEAS`` command and wait for its result notification. + .. note:: - The cellular network information included in the location request will be the one received from the ``AT%NCELLMEAS`` command. - This means that, for the most up-to-date location information, you should use the command as close to sending the location request as possible. - Also, keep in mind that whenever you send a location request in single-cell mode, any previously saved multi-cell cellular network information is invalidated. + Since the |SM| uses the ``AT%NCELLMEAS`` command internally, the host must not use the ``AT%NCELLMEAS`` command during ``#XNRFCLOUDPOS`` command execution. + You may still use ``AT%NCELLMEAS`` command before or after ``#XNRFCLOUDPOS`` command execution for your own purposes. + You will also see ``%NCELLMEAS`` notifications, which you can ignore, during the ``#XNRFCLOUDPOS`` command execution. -The ```` parameter can have the following integer values: +* The ```` parameter can have the following integer values: -* ``0`` - Do not include Wi-Fi access point information in the location request. -* ``1`` - Use Wi-Fi access point information. - The access points must be given as additional parameters to the command. - The minimum number of access points to provide is two (``NRF_CLOUD_LOCATION_WIFI_AP_CNT_MIN``), and the maximum is limited by the AT command buffer size (:ref:`CONFIG_SM_AT_BUF_SIZE `). + * ``0`` - Do not include Wi-Fi access point information in the location request. + * ``1`` - Use Wi-Fi access point information. + The access points must be given as additional parameters to the command. + The minimum number of access points to provide is two (``NRF_CLOUD_LOCATION_WIFI_AP_CNT_MIN``), and the maximum is limited by the AT command buffer size (:ref:`CONFIG_SM_AT_BUF_SIZE `). -The ```` parameter is a string. -It indicates the MAC address of a Wi-Fi access point and must be formatted as ``%02x:%02x:%02x:%02x:%02x:%02x`` (``WIFI_MAC_ADDR_TEMPLATE``). +* The ```` parameter is a string. + It indicates the MAC address of a Wi-Fi access point and must be formatted as ``%02x:%02x:%02x:%02x:%02x:%02x`` (``WIFI_MAC_ADDR_TEMPLATE``). -The ```` parameter is an optional integer. -It indicates the signal strength of a Wi-Fi access point in dBm, between ``-128`` and ``0``. -If provided, it must follow the MAC address parameter of the access point. -Providing the RSSI parameters helps improve the accuracy of the Wi-Fi location. +* The ```` parameter is an optional integer. + It indicates the signal strength of a Wi-Fi access point in dBm, between ``-128`` and ``0``. + If provided, it must follow the MAC address parameter of the access point. + Providing the RSSI parameters helps improve the accuracy of the Wi-Fi location. Unsolicited notification ~~~~~~~~~~~~~~~~~~~~~~~~ :: - #XNRFCLOUDPOS: - -This is emitted when the location request failed, either when sending it or receiving its response. -No notification containing location data will be emitted. + #XNRFCLOUDPOS: [,,,,] -* The ```` parameter indicates the error that happened. - It is one of the :c:enum:`nrf_cloud_error` values. - -:: +* The ```` parameter indicates the status of the location request. - #XNRFCLOUDPOS: ,,, + * ``0`` - Successful request. Other parameters are also present. + * ``-1`` - Location request failed. + * ```` - Requesting location from the cloud failed with cloud error as defined in :c:enum:`nrf_cloud_error` values. This is emitted when a successful response to a sent location request is received. @@ -269,27 +266,29 @@ Example OK - #XNRFCLOUDPOS: 0,35.455833,139.626111,1094 - AT%NCELLMEAS + #XNRFCLOUDPOS: 0,0,35.455833,139.626111,1094 + + AT#XNRFCLOUDPOS=5,0 OK %NCELLMEAS: 0,"0199F10A","44020","107E",65535,3750,5,49,27,107504,3750,251,33,4,0,475,107,26,14,25,475,58,26,17,25,475,277,24,9,25,475,51,18,1,25 - AT#XNRFCLOUDPOS=2,0 - OK + %NCELLMEAS: 0,"01234567","44020","0340",50,175456,3400,34,5,24,1775066,1,0,"00143FAE","44020","0140",65535,0,6200,47,40,14,1775066,0,0 + + %NCELLMEAS: 0,"00987654","44020","0240",50,1754746,5500,44,4,4,1463457,1,0,"002F4344","44020","0140",65535,0,6200,47,40,14,1775066,0,0,"001C0502","44013","5A00",65535,0,6400,130,29,18,1775124,0,0,"00136107","44013","5A00",65535,0,3600,202,26,13,234533,0,0 - #XNRFCLOUDPOS: 1,35.455833,139.626111,1094 + #XNRFCLOUDPOS: 0,1,35.455833,139.626111,1094 AT#XNRFCLOUDPOS=0,1,"40:9b:cd:c1:5a:40","00:90:fe:eb:4f:42" OK - #XNRFCLOUDPOS: 2,35.457335,139.624443,60 + #XNRFCLOUDPOS: 0,2,35.457335,139.624443,60 AT#XNRFCLOUDPOS=0,1,"40:9b:cd:c1:5a:40",-40,"00:90:fe:eb:4f:42",-69 OK - #XNRFCLOUDPOS: 2,35.457346,139.624449,20 + #XNRFCLOUDPOS: 0,2,35.457346,139.624449,20 Read command ------------ diff --git a/doc/releases/migration_notes_ncs_slm_v3.1.x.rst b/doc/releases/migration_notes_ncs_slm_v3.1.x.rst index 26ad2b42..c838fd59 100644 --- a/doc/releases/migration_notes_ncs_slm_v3.1.x.rst +++ b/doc/releases/migration_notes_ncs_slm_v3.1.x.rst @@ -359,6 +359,24 @@ PPP connection management changes * The :file:`overlay-ppp-cmux-linux.conf` overlay file. Use the :file:`overlay-ppp.conf` and :file:`overlay-cmux.conf` files instead. +Other changes +************* + + * ``AT#XNRFCLOUDPOS``: + + * Changed ```` parameter to ````. The meaning changes from no cell positioning, single-cell or multi-cell to the number of cells to be included in the location request. + ``0`` means that cellular positioning is not requested at all. + * The ``AT#XNRFCLOUDPOS`` command has been updated to use the ``AT%NCELLMEAS`` command internally, so the host must not use it anymore. + * ``#XNRFCLOUDPOS`` notification now includes the status of the location request. + The syntax has changed from: + :: + #XNRFCLOUDPOS: + #XNRFCLOUDPOS: ,,, + + to: + :: + #XNRFCLOUDPOS: [,,,,] + Removed features **************** diff --git a/doc/releases/migration_notes_v2.0.0.rst b/doc/releases/migration_notes_v2.0.0.rst index fe68af79..7498cf15 100644 --- a/doc/releases/migration_notes_v2.0.0.rst +++ b/doc/releases/migration_notes_v2.0.0.rst @@ -23,6 +23,20 @@ The following changes are mandatory to make your application work in the same wa * Full FOTA - When compiling, rename ``overlay-full_fota.conf`` to ``overlay-full-fota.conf`` and add ``overlay-full-fota.overlay`` to the build configuration. See :ref:`SM_AT_FOTA` for more information. +* ``AT#XNRFCLOUDPOS``: + + * Changed ```` parameter to ````. The meaning changes from no cell positioning, single-cell or multi-cell to the number of cells to be included in the location request. + ``0`` means that cellular positioning is not requested at all. + * The ``AT#XNRFCLOUDPOS`` command has been updated to use the ``AT%NCELLMEAS`` command internally, so the host must not use it anymore. + * ``#XNRFCLOUDPOS`` notification now includes the status of the location request. + The syntax has changed from: + :: + #XNRFCLOUDPOS: + #XNRFCLOUDPOS: ,,, + to: + :: + #XNRFCLOUDPOS: [,,,,] + Custom static partition layout migration ---------------------------------------- @@ -50,5 +64,6 @@ The following changes are listed for informational purposes, and many hosts will * Ring Indication (RI) - Change RI from pulse (100 ms) to level triggered, meaning RI stays asserted until the host asserts DTR. After the Serial Modem has enabled UART, RI will be deasserted. -* nRF Cloud transport has been changed from MQTT to CoAP. * HTTP client has been added and it's enabled by default. Use CONFIG_SM_HTTPC=n if you do not need it and want to save flash. +* nRF Cloud transport has been changed from MQTT to CoAP. +* ``CONFIG_SM_NRF_CLOUD_LOCATION`` is enabled by default whenever ``CONFIG_SM_NRF_CLOUD`` is enabled. Use ``CONFIG_SM_NRF_CLOUD_LOCATION=n`` if you do not need it and want to save flash. From 1dac63b65844ccf9799f50b51ff780f6ee8e6d10 Mon Sep 17 00:00:00 2001 From: Tommi Rantanen Date: Mon, 1 Jun 2026 14:17:00 +0300 Subject: [PATCH 2/2] app: tests: at_nrfcloud: Add new unit test set Add new unit tests for nRF Cloud related functionality. Signed-off-by: Tommi Rantanen --- app/src/sm_at_nrfcloud.c | 2 - app/tests/at_nrfcloud/CMakeLists.txt | 100 +++ app/tests/at_nrfcloud/include/date_time.h | 41 + app/tests/at_nrfcloud/include/net/nrf_cloud.h | 27 + .../at_nrfcloud/include/net/nrf_cloud_coap.h | 104 +++ .../include/net/nrf_cloud_location.h | 39 + .../include/net/wifi_location_common.h | 51 ++ .../include/nrf_cloud_coap_transport.h | 18 + app/tests/at_nrfcloud/prj.conf | 41 + app/tests/at_nrfcloud/src/nrf_cloud_stubs.c | 50 ++ .../at_nrfcloud/src/nrf_modem_at_wrapper.c | 74 ++ app/tests/at_nrfcloud/src/test_at_nrfcloud.c | 827 ++++++++++++++++++ app/tests/at_nrfcloud/testcase.yaml | 7 + 13 files changed, 1379 insertions(+), 2 deletions(-) create mode 100644 app/tests/at_nrfcloud/CMakeLists.txt create mode 100644 app/tests/at_nrfcloud/include/date_time.h create mode 100644 app/tests/at_nrfcloud/include/net/nrf_cloud.h create mode 100644 app/tests/at_nrfcloud/include/net/nrf_cloud_coap.h create mode 100644 app/tests/at_nrfcloud/include/net/nrf_cloud_location.h create mode 100644 app/tests/at_nrfcloud/include/net/wifi_location_common.h create mode 100644 app/tests/at_nrfcloud/include/nrf_cloud_coap_transport.h create mode 100644 app/tests/at_nrfcloud/prj.conf create mode 100644 app/tests/at_nrfcloud/src/nrf_cloud_stubs.c create mode 100644 app/tests/at_nrfcloud/src/nrf_modem_at_wrapper.c create mode 100644 app/tests/at_nrfcloud/src/test_at_nrfcloud.c create mode 100644 app/tests/at_nrfcloud/testcase.yaml diff --git a/app/src/sm_at_nrfcloud.c b/app/src/sm_at_nrfcloud.c index cec32550..423936c3 100644 --- a/app/src/sm_at_nrfcloud.c +++ b/app/src/sm_at_nrfcloud.c @@ -9,8 +9,6 @@ #include #include #include -#include -#include #include #include "nrf_cloud_coap_transport.h" #include diff --git a/app/tests/at_nrfcloud/CMakeLists.txt b/app/tests/at_nrfcloud/CMakeLists.txt new file mode 100644 index 00000000..ad936f39 --- /dev/null +++ b/app/tests/at_nrfcloud/CMakeLists.txt @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# +# Copyright (c) 2026 Nordic Semiconductor ASA + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(at_nrfcloud) + +# Generate sm_version.h header +set(GENERATED_DIR "${PROJECT_BINARY_DIR}/generated") +file(MAKE_DIRECTORY "${GENERATED_DIR}") +add_custom_target( + generate_version_header ALL + COMMAND ${CMAKE_COMMAND} + -D OUTPUT_FILE=${GENERATED_DIR}/sm_version.h + -P ${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/write_sm_version_header.cmake + BYPRODUCTS ${GENERATED_DIR}/sm_version.h + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../.. + COMMENT "Regenerating sm_version.h" +) +zephyr_include_directories("${GENERATED_DIR}") +zephyr_include_directories(${ZEPHYR_NRFXLIB_MODULE_DIR}/nrf_modem/include/) + +# Compiler options to set configuration values +target_compile_options(app PRIVATE + -DCONFIG_NRF_MODEM_LIB_MEM_DIAG=y + -D__ELASTERROR=2000 + -DCONFIG_SM_AT_BUF_SIZE=4096 + -DCONFIG_SM_URC_BUFFER_SIZE=4096 + -DCONFIG_SM_DATAMODE_BUF_SIZE=4096 + -DCONFIG_SM_DATAMODE_TERMINATOR=\"+++\" + -DCONFIG_SM_LOG_LEVEL=0 + -DCONFIG_SM_CR_LF_TERMINATION=1 + -DCONFIG_SM_CUSTOMER_VERSION=\"\" + -DCONFIG_SM_URC_DELAY_WITH_INCOMPLETE_ECHO_MS=1000 + -DCONFIG_SM_AT_ECHO_MAX_LEN=256 + -DCONFIG_SM_UART_RX_BUF_SIZE=256 + -DCONFIG_SM_UART_TX_BUF_SIZE=256 + -DCONFIG_SM_NRF_CLOUD_LOCATION=1 + -DCONFIG_NRF_CLOUD_SEC_TAG=16842753 +) + +# Generate CMock mocks +cmock_handle(${ZEPHYR_BASE}/../nrfxlib/nrf_modem/include/nrf_modem.h) +cmock_handle(${ZEPHYR_NRF_MODULE_DIR}/include/modem/nrf_modem_lib.h) +cmock_handle(${ZEPHYR_BASE}/../nrfxlib/nrf_modem/include/nrf_socket.h) +cmock_handle(${ZEPHYR_BASE}/../nrfxlib/nrf_modem/include/nrf_modem_at.h + FUNC_EXCLUDE "__nrf_modem_printf_like" + FUNC_EXCLUDE "__nrf_modem_scanf_like" + FUNC_EXCLUDE "nrf_modem_at_cmd" + # at_monitor_sys_init() calls this at SYS_INIT time, before CMock is + # initialised by setUp(). Exclude it from mocking and provide a manual + # stub in nrf_modem_at_wrapper.c instead. + ) +cmock_handle(${ZEPHYR_BASE}/include/zephyr/net/socket.h zephyr/net) +cmock_handle(${PROJECT_SOURCE_DIR}/../../src/sm_at_socket.h) +# Mock nRF Cloud CoAP API – the stub header carries all required type +# definitions directly; no REST-layer include chain is needed. +cmock_handle(${PROJECT_SOURCE_DIR}/include/net/nrf_cloud_coap.h) + +# Generate test runner +test_runner_generate(src/test_at_nrfcloud.c) + +# Add sources +target_sources(app PRIVATE + src/test_at_nrfcloud.c + src/nrf_modem_at_wrapper.c + src/nrf_cloud_stubs.c # date_time_update_async + nrf_cloud_client_id_get only + ../stubs/uart_stubs.c + ../stubs/sm_at_host_stubs.c + ../stubs/control_pin_stubs.c + ../stubs/pm_stubs.c + ../stubs/tfm_stubs.c + ../stubs/at_cmd_custom_stubs.c + ../stubs/sm_workq.c + ../stubs/sm_log_stubs.c + ../../src/sm_util.c + ../../src/sm_at_nrfcloud.c + ../../src/sm_at_host.c + ../../src/sm_at_commands.c + ${ZEPHYR_BASE}/subsys/modem/modem_pipe.c +) + +# Include directories: +# 1. Test-specific overrides (stub headers) come first so they shadow the +# real nrf_cloud / date_time / wifi headers. +# 2. Shared test include overrides (pm_config.h, fw_info.h, etc.) from the +# at_commands test. +# 3. Source directories and external include paths follow. +set(includes + "${PROJECT_SOURCE_DIR}/include/" + "${PROJECT_SOURCE_DIR}/../at_commands/include/" + "${PROJECT_SOURCE_DIR}/../../src" + "${ZEPHYR_BASE}/../nrfxlib/nrf_modem/include" + "${ZEPHYR_BASE}/include/" + "${PROJECT_SOURCE_DIR}/../stubs" +) + +target_include_directories(app PRIVATE ${includes}) diff --git a/app/tests/at_nrfcloud/include/date_time.h b/app/tests/at_nrfcloud/include/date_time.h new file mode 100644 index 00000000..96cee436 --- /dev/null +++ b/app/tests/at_nrfcloud/include/date_time.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/* + * Minimal stub for date_time.h used in unit tests. + * Only declares the symbols needed by sm_at_nrfcloud.c. + */ + +#ifndef DATE_TIME_TEST_STUB_H_ +#define DATE_TIME_TEST_STUB_H_ + +#include + +/** Event types reported by the date-time library. */ +enum date_time_evt_type { + DATE_TIME_OBTAINED_MODEM, + DATE_TIME_OBTAINED_NTP, + DATE_TIME_OBTAINED_EXT, + DATE_TIME_NOT_OBTAINED, +}; + +struct date_time_evt { + enum date_time_evt_type type; +}; + +typedef void (*date_time_evt_handler_t)(const struct date_time_evt *evt); + +/** + * @brief Stub: trigger async date/time update. + * + * In the test environment this is a no-op stub; the semaphore + * sem_date_time therefore times out (after K_SECONDS(10)) inside + * nrfcloud_conn_work_fn. Tests that exercise the full connect work + * should give the semaphore manually or wait for the timeout. + */ +int date_time_update_async(date_time_evt_handler_t evt_handler); + +#endif /* DATE_TIME_TEST_STUB_H_ */ diff --git a/app/tests/at_nrfcloud/include/net/nrf_cloud.h b/app/tests/at_nrfcloud/include/net/nrf_cloud.h new file mode 100644 index 00000000..700315dc --- /dev/null +++ b/app/tests/at_nrfcloud/include/net/nrf_cloud.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/* Minimal stub for nrf_cloud.h used in unit tests. */ + +#ifndef NRF_CLOUD_TEST_STUB_H_ +#define NRF_CLOUD_TEST_STUB_H_ + +#include + +/** Maximum length of the nRF Cloud client ID. */ +#define NRF_CLOUD_CLIENT_ID_MAX_LEN 64 + +/** + * @brief Get the nRF Cloud client identifier (device ID). + * + * @param[out] id_buf Buffer to receive the null-terminated device ID. + * @param[in] id_buf_sz Size of id_buf; must be at least + * NRF_CLOUD_CLIENT_ID_MAX_LEN + 1. + * @return 0 on success, negative errno on error. + */ +int nrf_cloud_client_id_get(char *id_buf, size_t id_buf_sz); + +#endif /* NRF_CLOUD_TEST_STUB_H_ */ diff --git a/app/tests/at_nrfcloud/include/net/nrf_cloud_coap.h b/app/tests/at_nrfcloud/include/net/nrf_cloud_coap.h new file mode 100644 index 00000000..8f6e6964 --- /dev/null +++ b/app/tests/at_nrfcloud/include/net/nrf_cloud_coap.h @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/* + * Stub for nrf_cloud_coap.h. + * + * Compilation requirement: full type definitions are pulled in through the + * normal include chain (lte_lc.h, wifi_location_common.h, nrf_cloud_location.h) + * exactly as in production code. + * + * CMock requirement: CMock's Ruby parser runs with WORKING_DIRECTORY set to + * the project source root and therefore cannot resolve the relative include + * paths. The parser skips includes it cannot find and parses only the + * function declarations that appear directly in this file — which is + * sufficient to generate correct mock stubs. + */ + +#ifndef NRF_CLOUD_COAP_TEST_STUB_H_ +#define NRF_CLOUD_COAP_TEST_STUB_H_ + +#include +#include +#include +#include + +/* + * Pull in the full type definitions that sm_at_nrfcloud.c needs when it + * accesses struct members (wifi_scan_info, lte_lc_cells_info, etc.). + * CMock's Ruby parser will skip these includes if it cannot find them; the + * function signatures below are enough for mock generation. + */ +#include +#include "net/wifi_location_common.h" +#include "net/nrf_cloud_location.h" + +/* ---------------------------------------------------------------------- + * CoAP content-format constant. + * In production code this comes from when + * CONFIG_NRF_CLOUD_COAP is set. Without that Kconfig the real header + * defines a stub enum without the actual values, so we define it here. + * ---------------------------------------------------------------------- + */ +#ifndef COAP_CONTENT_FORMAT_APP_JSON +#define COAP_CONTENT_FORMAT_APP_JSON 50 +#endif + +/* ---------------------------------------------------------------------- + * CoAP response callback – matches the real coap_client_response_cb_t. + * ---------------------------------------------------------------------- + */ +#ifndef COAP_CLIENT_RESPONSE_CB_T_DEFINED +#define COAP_CLIENT_RESPONSE_CB_T_DEFINED +typedef void (*coap_client_response_cb_t)(int16_t result_code, size_t offset, + const uint8_t *payload, size_t len, + bool last_block, void *user_data); +#endif + +/* ---------------------------------------------------------------------- + * Wi-Fi and location constants (also defined in the sub-stubs; guards + * prevent redefinition when this header is included first). + * ---------------------------------------------------------------------- + */ +#ifndef NRF_CLOUD_LOCATION_WIFI_AP_CNT_MIN +#define NRF_CLOUD_LOCATION_WIFI_AP_CNT_MIN 2 +#endif +#ifndef NRF_CLOUD_LOCATION_WIFI_OMIT_RSSI +#define NRF_CLOUD_LOCATION_WIFI_OMIT_RSSI (INT8_MAX) +#endif + +/* ---------------------------------------------------------------------- + * nRF Cloud CoAP location request – matches struct nrf_cloud_coap_location_request + * in the real nrf_cloud_coap.h. + * ---------------------------------------------------------------------- + */ +#ifndef NRF_CLOUD_COAP_LOCATION_REQUEST_DEFINED +#define NRF_CLOUD_COAP_LOCATION_REQUEST_DEFINED +/* Forward declaration; the test always passes config = NULL. */ +struct nrf_cloud_location_config; + +struct nrf_cloud_coap_location_request { + struct lte_lc_cells_info *cell_info; + struct wifi_scan_info *wifi_info; + const struct nrf_cloud_location_config *config; +}; +#endif /* NRF_CLOUD_COAP_LOCATION_REQUEST_DEFINED */ + +/* ---------------------------------------------------------------------- + * nRF Cloud CoAP API – mocked by CMock + * ---------------------------------------------------------------------- + */ +int nrf_cloud_coap_init(void); +int nrf_cloud_coap_connect(const char *const app_ver); +int nrf_cloud_coap_disconnect(void); +int nrf_cloud_coap_location_get(struct nrf_cloud_coap_location_request const *const request, + struct nrf_cloud_location_result *const result); +int nrf_cloud_coap_post(const char *resource, const char *query, + const uint8_t *buf, size_t len, + int fmt, bool reliable, + coap_client_response_cb_t cb, void *user); + +#endif /* NRF_CLOUD_COAP_TEST_STUB_H_ */ diff --git a/app/tests/at_nrfcloud/include/net/nrf_cloud_location.h b/app/tests/at_nrfcloud/include/net/nrf_cloud_location.h new file mode 100644 index 00000000..9ac293c7 --- /dev/null +++ b/app/tests/at_nrfcloud/include/net/nrf_cloud_location.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/* Minimal stub for nrf_cloud_location.h used in unit tests. */ + +#ifndef NRF_CLOUD_LOCATION_TEST_STUB_H_ +#define NRF_CLOUD_LOCATION_TEST_STUB_H_ + +#include +#include + +/** Minimum number of access points required for a Wi-Fi location request. */ +#define NRF_CLOUD_LOCATION_WIFI_AP_CNT_MIN 2 + +/** Sentinel RSSI value indicating that RSSI should be omitted from the request. */ +#define NRF_CLOUD_LOCATION_WIFI_OMIT_RSSI (INT8_MAX) + +/** @brief Location result type from nRF Cloud (matches real SDK enum order) */ +#ifndef NRF_CLOUD_LOCATION_TYPE_DEFINED +#define NRF_CLOUD_LOCATION_TYPE_DEFINED +enum nrf_cloud_location_type { + LOCATION_TYPE_SINGLE_CELL = 0, + LOCATION_TYPE_MULTI_CELL, + LOCATION_TYPE_WIFI, + LOCATION_TYPE__INVALID, +}; + +struct nrf_cloud_location_result { + enum nrf_cloud_location_type type; + double lat; + double lon; + uint32_t unc; +}; +#endif /* NRF_CLOUD_LOCATION_TYPE_DEFINED */ + +#endif /* NRF_CLOUD_LOCATION_TEST_STUB_H_ */ diff --git a/app/tests/at_nrfcloud/include/net/wifi_location_common.h b/app/tests/at_nrfcloud/include/net/wifi_location_common.h new file mode 100644 index 00000000..ddf6fa77 --- /dev/null +++ b/app/tests/at_nrfcloud/include/net/wifi_location_common.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/* Minimal stub for wifi_location_common.h used in unit tests. */ + +#ifndef WIFI_LOCATION_COMMON_TEST_STUB_H_ +#define WIFI_LOCATION_COMMON_TEST_STUB_H_ + +#include + +#ifndef WIFI_MAC_ADDR_LEN +#define WIFI_MAC_ADDR_LEN 6 +#endif + +#ifndef WIFI_MAC_ADDR_STR_LEN +/* 2 chars per byte + 5 colons */ +#define WIFI_MAC_ADDR_STR_LEN ((WIFI_MAC_ADDR_LEN * 2) + 5) +#endif + +#ifndef WIFI_MAC_ADDR_TEMPLATE +#define WIFI_MAC_ADDR_TEMPLATE "%x:%x:%x:%x:%x:%x" +#endif + +#ifndef WIFI_SECURITY_TYPE_UNKNOWN +#define WIFI_SECURITY_TYPE_UNKNOWN 0 +#endif + +#ifndef WIFI_MFP_UNKNOWN +#define WIFI_MFP_UNKNOWN 0 +#endif + +/** @brief Access point information from a Wi-Fi scan. */ +struct wifi_scan_result { + uint8_t mac[WIFI_MAC_ADDR_LEN]; + uint8_t mac_length; + int8_t rssi; + uint8_t band; + int security; + int mfp; +}; + +/** @brief Access points found during a Wi-Fi scan. */ +struct wifi_scan_info { + struct wifi_scan_result *ap_info; + uint16_t cnt; +}; + +#endif /* WIFI_LOCATION_COMMON_TEST_STUB_H_ */ diff --git a/app/tests/at_nrfcloud/include/nrf_cloud_coap_transport.h b/app/tests/at_nrfcloud/include/nrf_cloud_coap_transport.h new file mode 100644 index 00000000..04b0cb9b --- /dev/null +++ b/app/tests/at_nrfcloud/include/nrf_cloud_coap_transport.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/* + * Stub for nrf_cloud_coap_transport.h. + * Provides the nrf_cloud_coap_post() declaration via the nrf_cloud_coap.h stub + * so that sm_at_nrfcloud.c compiles in the unit test environment. + */ + +#ifndef NRF_CLOUD_COAP_TRANSPORT_TEST_STUB_H_ +#define NRF_CLOUD_COAP_TRANSPORT_TEST_STUB_H_ + +#include "net/nrf_cloud_coap.h" + +#endif /* NRF_CLOUD_COAP_TRANSPORT_TEST_STUB_H_ */ diff --git a/app/tests/at_nrfcloud/prj.conf b/app/tests/at_nrfcloud/prj.conf new file mode 100644 index 00000000..f70e043f --- /dev/null +++ b/app/tests/at_nrfcloud/prj.conf @@ -0,0 +1,41 @@ +# +# Copyright (c) 2026 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +# + +CONFIG_UNITY=y +CONFIG_ASSERT=n + +CONFIG_ASAN=y + +CONFIG_DEBUG=y +CONFIG_NO_OPTIMIZATIONS=y + +CONFIG_RING_BUFFER=y +CONFIG_REBOOT=y +CONFIG_EVENTS=y +CONFIG_HEAP_MEM_POOL_SIZE=32768 + +CONFIG_AT_PARSER=y + +# Increase AT monitor heap because %NCELLMEAS notifications can be large +CONFIG_AT_MONITOR=y +CONFIG_AT_MONITOR_HEAP_SIZE=1024 + +# Logging +CONFIG_LOG=y +CONFIG_LOG_MODE_IMMEDIATE=y +CONFIG_LOG_PRINTK=y +CONFIG_LOG_BACKEND_SHOW_COLOR=n +CONFIG_LOG_BACKEND_FORMAT_TIMESTAMP=n + +# Native sim settings +CONFIG_NATIVE_SIM_SLOWDOWN_TO_REAL_TIME=n + +#CONFIG_MAIN_STACK_SIZE=4096 +#CONFIG_IDLE_STACK_SIZE=4096 +#CONFIG_ISR_STACK_SIZE=4096 +#CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096 +#CONFIG_MPSL_WORK_STACK_SIZE=4096 +#CONFIG_ARCH_POSIX_RECOMMENDED_STACK_SIZE=1024 diff --git a/app/tests/at_nrfcloud/src/nrf_cloud_stubs.c b/app/tests/at_nrfcloud/src/nrf_cloud_stubs.c new file mode 100644 index 00000000..1e955834 --- /dev/null +++ b/app/tests/at_nrfcloud/src/nrf_cloud_stubs.c @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/** + * @file nrf_cloud_stubs.c + * + * Manual stubs for nRF Cloud functions that are NOT mocked via CMock: + * + * - nrf_cloud_client_id_get() (declared in net/nrf_cloud.h) + * - date_time_update_async() (declared in date_time.h) + * + * All nrf_cloud_coap_* functions are generated by CMock from the + * include/net/nrf_cloud_coap.h stub header (see CMakeLists.txt). + */ + +#include +#include +#include +#include "net/nrf_cloud.h" +#include "date_time.h" + +/* Device-ID returned by nrf_cloud_client_id_get(). */ +static const char *device_id_stub = "test-device-id"; + +int nrf_cloud_client_id_get(char *id_buf, size_t id_buf_sz) +{ + if (id_buf && id_buf_sz > 0) { + strncpy(id_buf, device_id_stub, id_buf_sz - 1); + id_buf[id_buf_sz - 1] = '\0'; + } + return 0; +} + +int date_time_update_async(date_time_evt_handler_t evt_handler) +{ + /* + * Immediately invoke the handler with DATE_TIME_OBTAINED_NTP so that + * nrfcloud_conn_work_fn's k_sem_take(&sem_date_time, K_SECONDS(10)) + * returns at once instead of timing out. + */ + if (evt_handler != NULL) { + struct date_time_evt evt = {.type = DATE_TIME_OBTAINED_NTP}; + + evt_handler(&evt); + } + return 0; +} diff --git a/app/tests/at_nrfcloud/src/nrf_modem_at_wrapper.c b/app/tests/at_nrfcloud/src/nrf_modem_at_wrapper.c new file mode 100644 index 00000000..45ddae77 --- /dev/null +++ b/app/tests/at_nrfcloud/src/nrf_modem_at_wrapper.c @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/** + * @file nrf_modem_at_wrapper.c + * + * Routes AT commands to the custom handlers registered by sm_at_nrfcloud.c + * (and the other source files included in this test build). + * + * The AT modem command nrf_modem_at_cmd() is used internally by sm_util.c + * (sm_util_at_printf / sm_util_at_scanf). This wrapper intercepts those + * calls and: + * - Routes #XNRFCLOUD / #XNRFCLOUDPOS to the SM handler wrappers. + * - Responds to AT%NCELLMEAS=1|3|4 with a preconfigured response string + * so that the work-queue-driven ncellmeas flow can be driven from tests. + * - Responds to AT+CSCON? (RRC state query) with RRC-idle. + * - Returns -NRF_EINVAL for unknown commands. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Handler wrappers generated by AT_CMD_CUSTOM in sm_at_nrfcloud.c */ +extern int handle_at_nrf_cloud_wrapper_xnrfcloud(char *buf, size_t len, char *at_cmd); +extern int handle_at_nrf_cloud_pos_wrapper_xnrfcloudpos(char *buf, size_t len, char *at_cmd); + +char test_at_nrfcloud_ncellmeas_resp[256]; +int test_at_nrfcloud_ncellmeas_resp_ret; + +int nrf_modem_at_cmd(void *buf, size_t buf_size, const char *fmt, ...) +{ + char at_cmd[256]; + va_list args; + int ret; + + va_start(args, fmt); + vsnprintf(at_cmd, sizeof(at_cmd), fmt, args); + va_end(args); + + if (strncasecmp(at_cmd, "AT#XNRFCLOUDPOS", 15) == 0) { + ret = handle_at_nrf_cloud_pos_wrapper_xnrfcloudpos( + (char *)buf, buf_size, at_cmd); + } else if (strncasecmp(at_cmd, "AT#XNRFCLOUD", 12) == 0) { + ret = handle_at_nrf_cloud_wrapper_xnrfcloud( + (char *)buf, buf_size, at_cmd); + } else if (strncasecmp(at_cmd, "AT%NCELLMEAS", 12) == 0) { + /* Simulate sending the AT%NCELLMEAS command to the modem. + * Return "OK" to sm_util_at_printf. + */ + if (buf && buf_size > 0) { + strncpy((char *)buf, test_at_nrfcloud_ncellmeas_resp, buf_size - 1); + } + ret = test_at_nrfcloud_ncellmeas_resp_ret; + } else if (strncasecmp(at_cmd, "AT+CSCON?", 9) == 0) { + /* RRC connection state: 0 = idle */ + if (buf && buf_size > 0) { + snprintf((char *)buf, buf_size, "+CSCON: 0,0\r\nOK\r\n"); + } + ret = 0; + } else { + ret = -NRF_EINVAL; + } + + return ret; +} diff --git a/app/tests/at_nrfcloud/src/test_at_nrfcloud.c b/app/tests/at_nrfcloud/src/test_at_nrfcloud.c new file mode 100644 index 00000000..eee6a877 --- /dev/null +++ b/app/tests/at_nrfcloud/src/test_at_nrfcloud.c @@ -0,0 +1,827 @@ +/* + * Copyright (c) 2026 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +/** + * @file test_at_nrfcloud.c + * + * Unit tests for sm_at_nrfcloud.c. + * + * Covers: + * - AT#XNRFCLOUD – connect / disconnect / send / read / test operations + * - AT#XNRFCLOUDPOS – parameter validation, success path, and URC output + * - %NCELLMEAS parsing (search types 1/2 and GCI 3/4) via the + * CONFIG_UNITY-visible wrappers sm_at_nrfcloud_test_parse_ncellmeas() + * and sm_at_nrfcloud_test_parse_ncellmeas_gci(). + * + * %NCELLMEAS test data is taken from the nRF Location library test suite + * (nrf/tests/lib/location/src/location_test.c). + * + * CMock naming convention used in this project + * -------------------------------------------- + * The cmock_handle() macro uses the linker --defsym trick to rename the real + * symbol foo() → __cmock_foo(). + * CMock then provides a new foo() implementation (the mock). + * + * As a result the CMock helper macros are prefixed with __cmock_, e.g.: + * __cmock_nrf_cloud_coap_connect_ExpectAnyArgsAndReturn(0) + * __cmock_nrf_cloud_coap_location_get_ReturnThruPtr_result(&r) + * + * The Init / Verify functions use the cmock_ prefix: + * cmock_nrf_cloud_coap_Init() + * cmock_nrf_cloud_coap_Verify() + */ + +#include +#include +#include +#include +#include +#include + +#include "sm_at_host.h" +#include "sm_at_nrfcloud.h" +#include "uart_stub.h" + +/* CMock-generated mocks */ +#include "cmock_nrf_modem_at.h" +#include "cmock_nrf_cloud_coap.h" + + +static const char *resp; +static char *result; +extern char test_at_nrfcloud_ncellmeas_resp[]; +extern int test_at_nrfcloud_ncellmeas_resp_ret; + +/* --------------------------------------------------------------------------- + * Externals provided by the stub / helper files + * --------------------------------------------------------------------------- + */ +extern const char *get_captured_response(void); +extern size_t get_captured_response_len(void); +extern void clear_captured_response(void); + +/* at_monitor_dispatch() is implemented in at_monitor library and + * we'll call it directly to fake received AT commands/notifications + */ +extern void at_monitor_dispatch(const char *at_notif); + +/* Strings for cellular positioning */ +static const char ncellmeas_resp_pci1[] = + "%NCELLMEAS:0,\"00011B07\",\"26295\",\"00B7\",2300,7,63,31," + "150344527,2300,8,60,29,0,2400,11,55,26,184\r\n"; + +static const char ncellmeas_resp_gci1[] = + "%NCELLMEAS:0,\"00011B07\",\"26295\",\"00B7\",10512,9034,2300,7,63,31,150344527,1,0," + "\"00011B08\",\"26295\",\"00B7\",65535,0,2300,9,62,30,150345527,0,0\r\n"; + +static const char ncellmeas_resp_gci5[] = + "%NCELLMEAS:0,\"00011B07\",\"26295\",\"00B7\",10512,9034,2300,7,63,31,150344527,1,0," + "\"00011B66\",\"26287\",\"00C3\",65535,0,4300,6,71,30,150345527,0,0," + "\"0002ABCD\",\"26287\",\"00C3\",65535,0,4300,6,71,30,150345527,0,0," + "\"00103425\",\"26244\",\"0056\",65535,0,6400,6,71,30,150345527,0,0," + "\"00076543\",\"26256\",\"00C3\",65535,0,62000,6,71,30,150345527,0,0," + "\"00011B08\",\"26295\",\"00B7\",65535,0,2300,9,62,30,150345527,0,0\r\n"; + +/* Normal NCELLMEAS without neighbor cells: only serving cell data. */ +static const char ncellmeas_resp_no_neighbors[] = + "%NCELLMEAS:0,\"00011B07\",\"26295\",\"00B7\",500,6400,71,61,35," + "135488527\r\n"; + +/* --------------------------------------------------------------------------- + * setUp / tearDown + * --------------------------------------------------------------------------- + */ + +void setUp(void) +{ + clear_captured_response(); + + cmock_nrf_modem_at_Init(); + cmock_nrf_cloud_coap_Init(); + strcpy(test_at_nrfcloud_ncellmeas_resp, "\r\nOK\r\n"); + test_at_nrfcloud_ncellmeas_resp_ret = 0; +} + +void tearDown(void) +{ + /* Drain any pending work to avoid interference between tests. */ + k_sleep(K_MSEC(1)); + + cmock_nrf_modem_at_Verify(); + cmock_nrf_cloud_coap_Verify(); +} + +/* --------------------------------------------------------------------------- + * Helper functions + * --------------------------------------------------------------------------- + */ + +void helper_xnrfcloud_connect_ok(void) +{ + /* nrfcloud_conn_work_fn calls nrf_cloud_coap_connect(NULL). */ + __cmock_nrf_cloud_coap_connect_ExpectAnyArgsAndReturn(0); + send_at_command("AT#XNRFCLOUD=1\r\n"); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + TEST_ASSERT_NULL(strstr(resp, "ERROR")); + + result = strstr(resp, "#XNRFCLOUD"); + TEST_ASSERT_EQUAL_STRING("#XNRFCLOUD: 1,0\r\n", result); +} + +void helper_xnrfcloud_disconnect_ok(void) +{ + __cmock_nrf_cloud_coap_disconnect_ExpectAndReturn(0); + + send_at_command("AT#XNRFCLOUD=0\r\n"); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + TEST_ASSERT_NULL(strstr(resp, "ERROR")); + + result = strstr(resp, "#XNRFCLOUD"); + TEST_ASSERT_EQUAL_STRING("#XNRFCLOUD: 0,0\r\n", result); +} + +/* --------------------------------------------------------------------------- + * AT#XNRFCLOUD tests + * --------------------------------------------------------------------------- + */ + +/* + * Tests test command. + */ +void test_xnrfcloud_test_cmd(void) +{ + send_at_command("AT#XNRFCLOUD=?\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "#XNRFCLOUD: (0,1,2),\r\n\r\nOK\r\n")); +} + +/* + * Tests read command when nRF Cloud is not connected. + */ +void test_xnrfcloud_read_disconnected(void) +{ + send_at_command("AT#XNRFCLOUD?\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "#XNRFCLOUD: 0,0,16842753,\"\"\r\n\r\nOK\r\n")); +} + +/* + * Tests connect. + */ +void test_xnrfcloud_connect_ok(void) +{ + sm_nrf_cloud_ready = false; + + helper_xnrfcloud_connect_ok(); + + clear_captured_response(); + + send_at_command("AT#XNRFCLOUD?\r\n"); + k_sleep(K_MSEC(1)); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "#XNRFCLOUD: 1,0,16842753,\"\"\r\n\r\nOK\r\n")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests connect with = 1. + */ +void test_xnrfcloud_connect_with_send_location(void) +{ + __cmock_nrf_cloud_coap_connect_ExpectAnyArgsAndReturn(0); + + send_at_command("AT#XNRFCLOUD=1,1\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + k_sleep(K_MSEC(1)); + + clear_captured_response(); + send_at_command("AT#XNRFCLOUD?\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "#XNRFCLOUD: 1,1,16842753,\"\"\r\n\r\nOK\r\n")); + + /* Disconnect */ + clear_captured_response(); + __cmock_nrf_cloud_coap_disconnect_ExpectAndReturn(0); + + send_at_command("AT#XNRFCLOUD=0\r\n"); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + TEST_ASSERT_NULL(strstr(resp, "ERROR")); + + result = strstr(resp, "#XNRFCLOUD"); + TEST_ASSERT_EQUAL_STRING("#XNRFCLOUD: 0,1\r\n", result); +} + +/* + * Tests invalid value. + */ +void test_xnrfcloud_connect_invalid_send_location(void) +{ + send_at_command("AT#XNRFCLOUD=1,2\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); +} + +/* + * Tests connect when there is nRF Cloud connection already. + */ +void test_xnrfcloud_connect_already_connected(void) +{ + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + send_at_command("AT#XNRFCLOUD=1\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests disconnect. + */ +void test_xnrfcloud_disconnect_ok(void) +{ + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests disconnect when nRF Cloud is not connected. + */ +void test_xnrfcloud_disconnect_not_connected(void) +{ + send_at_command("AT#XNRFCLOUD=0\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); +} + +/* + * Tests sending data to cloud with AT#XNRFCLOUD=2 when nRF Cloud is not connected. + */ +void test_xnrfcloud_send_not_connected(void) +{ + send_at_command("AT#XNRFCLOUD=2\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); +} + +/* + * Tests invalid operation value. + */ +void test_xnrfcloud_invalid_op(void) +{ + send_at_command("AT#XNRFCLOUD=3\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); +} + +/* --------------------------------------------------------------------------- + * AT#XNRFCLOUDPOS tests + * --------------------------------------------------------------------------- + */ + +/* + * Tests AT#XNRFCLOUDPOS when nRF Cloud is not connected. + */ +void test_xnrfcloudpos_not_connected(void) +{ + send_at_command("AT#XNRFCLOUDPOS=1,0\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); +} + +/* + * Tests read command not supported. + */ +void test_xnrfcloudpos_read_not_supported(void) +{ + send_at_command("AT#XNRFCLOUDPOS?\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); +} + +/* + * Tests test command not supported. + */ +void test_xnrfcloudpos_test_not_supported(void) +{ + send_at_command("AT#XNRFCLOUDPOS=?\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); +} + +/* + * Tests no positioning method requested. + */ +void test_xnrfcloudpos_no_pos_method(void) +{ + send_at_command("AT#XNRFCLOUDPOS=0,0\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); +} + +/* + * Tests too big value. + */ +void test_xnrfcloudpos_cell_count_too_high(void) +{ + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + send_at_command("AT#XNRFCLOUDPOS=16,0\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests invalid value. + */ +void test_xnrfcloudpos_wifi_pos_invalid(void) +{ + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + send_at_command("AT#XNRFCLOUDPOS=0,2\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests missing parameter. + */ +void test_xnrfcloudpos_missing_params(void) +{ + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + send_at_command("AT#XNRFCLOUDPOS=0\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests no Wi-Fi but APs given. + */ +void test_xnrfcloudpos_no_wifi_but_ap_params(void) +{ + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + send_at_command("AT#XNRFCLOUDPOS=1,0,\"AA:BB:CC:DD:EE:FF\"\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests no Wi-Fi APs. + */ +void test_xnrfcloudpos_wifi_no_aps(void) +{ + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + send_at_command("AT#XNRFCLOUDPOS=0,1\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests invalid WiFi MAC address. + */ +void test_xnrfcloudpos_wifi_invalid_mac(void) +{ + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + send_at_command( + "AT#XNRFCLOUDPOS=0,1,\"GG:GG:GG:GG:GG:GG\",\"HH:11:22:33:44:55\"\r\n"); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests #XNRFCLOUDPOS with one AT%NCELLMEAS command. + */ +void test_xnrfcloudpos_cell_1ncellmeas_ok(void) +{ + static struct nrf_cloud_location_result loc_result = { + .type = LOCATION_TYPE_SINGLE_CELL, + .lat = 60.1699, + .lon = 24.9384, + .unc = 1000, + }; + + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + /* Expect exactly one call to location_get; fill *result with loc_result. */ + __cmock_nrf_cloud_coap_location_get_ExpectAnyArgsAndReturn(0); + __cmock_nrf_cloud_coap_location_get_ReturnThruPtr_result(&loc_result); + + send_at_command("AT#XNRFCLOUDPOS=1,0\r\n"); + + resp = get_captured_response(); + + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + TEST_ASSERT_NULL(strstr(resp, "ERROR")); + + /* NCELLMEAS notification */ + k_sleep(K_MSEC(1)); + at_monitor_dispatch(ncellmeas_resp_no_neighbors); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + result = strstr(resp, "#XNRFCLOUDPOS"); + TEST_ASSERT_EQUAL_STRING("#XNRFCLOUDPOS: 0,0,60.169900,24.938400,1000\r\n", result); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests #XNRFCLOUDPOS with 3 AT%NCELLMEAS commands. + */ +void test_xnrfcloudpos_cell_3ncellmeas_ok(void) +{ + static struct nrf_cloud_location_result loc_result = { + .type = LOCATION_TYPE_MULTI_CELL, + .lat = 12.345678, + .lon = -57.987654, + .unc = 1234, + }; + + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + /* Expect exactly one call to location_get; fill *result with loc_result. */ + __cmock_nrf_cloud_coap_location_get_ExpectAnyArgsAndReturn(0); + __cmock_nrf_cloud_coap_location_get_ReturnThruPtr_result(&loc_result); + + send_at_command("AT#XNRFCLOUDPOS=15,0\r\n"); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + TEST_ASSERT_NULL(strstr(resp, "ERROR")); + + /* NCELLMEAS notifications */ + k_sleep(K_MSEC(1)); + at_monitor_dispatch(ncellmeas_resp_pci1); + k_sleep(K_MSEC(1)); + at_monitor_dispatch(ncellmeas_resp_gci1); + k_sleep(K_MSEC(1)); + at_monitor_dispatch(ncellmeas_resp_gci5); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + result = strstr(resp, "#XNRFCLOUDPOS"); + TEST_ASSERT_EQUAL_STRING_LEN("#XNRFCLOUDPOS: 0,1,12.345678,-57.987654,1234\r\n", result, + strlen("#XNRFCLOUDPOS: 0,1,12.345678,-57.987654,1234\r\n")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests #XNRFCLOUDPOS with 2 AT%NCELLMEAS commands. + */ +void test_xnrfcloudpos_cell_2ncellmeas_ok(void) +{ + static struct nrf_cloud_location_result loc_result = { + .type = LOCATION_TYPE_MULTI_CELL, + .lat = 12.345678, + .lon = -57.987654, + .unc = 1234, + }; + + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + /* Expect exactly one call to location_get; fill *result with loc_result. */ + __cmock_nrf_cloud_coap_location_get_ExpectAnyArgsAndReturn(0); + __cmock_nrf_cloud_coap_location_get_ReturnThruPtr_result(&loc_result); + + send_at_command("AT#XNRFCLOUDPOS=4,0\r\n"); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + TEST_ASSERT_NULL(strstr(resp, "ERROR")); + + /* NCELLMEAS notifications */ + k_sleep(K_MSEC(1)); + at_monitor_dispatch(ncellmeas_resp_pci1); + k_sleep(K_MSEC(1)); + at_monitor_dispatch(ncellmeas_resp_gci5); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + result = strstr(resp, "#XNRFCLOUDPOS"); + TEST_ASSERT_EQUAL_STRING_LEN("#XNRFCLOUDPOS: 0,1,12.345678,-57.987654,1234\r\n", result, + strlen("#XNRFCLOUDPOS: 0,1,12.345678,-57.987654,1234\r\n")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests failing AT%NCELLMEAS command. + */ +void test_xnrfcloudpos_cell_ncellmeas_fail_ok(void) +{ + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + strcpy(test_at_nrfcloud_ncellmeas_resp, "\r\nERROR\r\n"); + test_at_nrfcloud_ncellmeas_resp_ret = -1; + + send_at_command("AT#XNRFCLOUDPOS=1,0\r\n"); + k_sleep(K_MSEC(1)); + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + TEST_ASSERT_NULL(strstr(resp, "ERROR")); + + result = strstr(resp, "#XNRFCLOUDPOS"); + TEST_ASSERT_EQUAL_STRING("#XNRFCLOUDPOS: -1\r\n", result); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests %NCELLMEAS notification having failure status. + */ +void test_xnrfcloudpos_cell_ncellmeas_notif_fail(void) +{ + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + send_at_command("AT#XNRFCLOUDPOS=4,0\r\n"); + /* Test that unsolicited NCELLMEAS notifications are ignored before + * the first AT%NCELLMEAS commands. This gets to the NCELLMEAS handler + * before the first AT%NCELLMEAS command because we don't sleep between + * AT#XNRFCLOUDPOS and at_monitor_dispatch(). + */ + at_monitor_dispatch(ncellmeas_resp_pci1); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + TEST_ASSERT_NULL(strstr(resp, "ERROR")); + + /* NCELLMEAS notifications */ + k_sleep(K_MSEC(1)); + at_monitor_dispatch("%NCELLMEAS:1\r\n"); + k_sleep(K_MSEC(1)); + at_monitor_dispatch("%NCELLMEAS:notnumber\r\n"); + k_sleep(K_MSEC(1)); + at_monitor_dispatch("%NCELLMEAS:1\r\n"); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + result = strstr(resp, "#XNRFCLOUDPOS"); + TEST_ASSERT_EQUAL_STRING_LEN("#XNRFCLOUDPOS: -1\r\n", result, + strlen("#XNRFCLOUDPOS: -1\r\n")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests failing cloud location request. + */ +void test_xnrfcloudpos_cell_cloud_request_fail(void) +{ + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + /* Expect exactly one call to location_get; fill *result with loc_result. */ + __cmock_nrf_cloud_coap_location_get_ExpectAnyArgsAndReturn(40100); + + send_at_command("AT#XNRFCLOUDPOS=4,0\r\n"); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + TEST_ASSERT_NULL(strstr(resp, "ERROR")); + + /* NCELLMEAS notifications */ + k_sleep(K_MSEC(1)); + at_monitor_dispatch(ncellmeas_resp_pci1); + k_sleep(K_MSEC(1)); + at_monitor_dispatch(ncellmeas_resp_gci5); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + result = strstr(resp, "#XNRFCLOUDPOS"); + TEST_ASSERT_EQUAL_STRING_LEN("#XNRFCLOUDPOS: 40100\r\n", result, + strlen("#XNRFCLOUDPOS: 40100\r\n")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests failing AT%NCELLMEAS command when Wi-Fi APs available. + */ +void test_xnrfcloudpos_cell_ncellmeas_notif_fail_wifi_ok(void) +{ + static struct nrf_cloud_location_result loc_result = { + .type = LOCATION_TYPE_WIFI, + .lat = 60.1699, + .lon = 24.9384, + .unc = 50, + }; + + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + __cmock_nrf_cloud_coap_location_get_ExpectAnyArgsAndReturn(0); + __cmock_nrf_cloud_coap_location_get_ReturnThruPtr_result(&loc_result); + + send_at_command("AT#XNRFCLOUDPOS=4,1,\"C0:FF:EE:00:11:22\",\"DE:AD:BE:EF:CA:FE\"\r\n"); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + TEST_ASSERT_NULL(strstr(resp, "ERROR")); + + /* NCELLMEAS notifications */ + k_sleep(K_MSEC(1)); + at_monitor_dispatch("%NCELLMEAS:1\r\n"); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + result = strstr(resp, "#XNRFCLOUDPOS"); + TEST_ASSERT_EQUAL_STRING("#XNRFCLOUDPOS: 0,2,60.169900,24.938400,50\r\n", result); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests Wi-Fi only positioning. + */ +void test_xnrfcloudpos_wifi_only_ok(void) +{ + static struct nrf_cloud_location_result loc_result = { + .type = LOCATION_TYPE_WIFI, + .lat = 60.1699, + .lon = 24.9384, + .unc = 50, + }; + + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + __cmock_nrf_cloud_coap_location_get_ExpectAnyArgsAndReturn(0); + __cmock_nrf_cloud_coap_location_get_ReturnThruPtr_result(&loc_result); + + k_sleep(K_MSEC(1)); + send_at_command("AT#XNRFCLOUDPOS=0,1,\"C0:FF:EE:00:11:22\",\"DE:AD:BE:EF:CA:FE\"\r\n"); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + TEST_ASSERT_NULL(strstr(resp, "ERROR")); + + k_sleep(K_MSEC(1)); + resp = get_captured_response(); + result = strstr(resp, "#XNRFCLOUDPOS"); + TEST_ASSERT_EQUAL_STRING("#XNRFCLOUDPOS: 0,2,60.169900,24.938400,50\r\n", result); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests failure when only one Wi-Fi AP given. + */ +void test_xnrfcloudpos_wifi_only_one_ap(void) +{ + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + send_at_command("AT#XNRFCLOUDPOS=0,1,\"AA:BB:CC:DD:EE:FF\",-60\r\n"); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + + TEST_ASSERT_NOT_NULL(strstr(resp, "ERROR")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* + * Tests combined cellular and Wi-Fi positioning. + */ +void test_xnrfcloudpos_cell_and_wifi_ok(void) +{ + static struct nrf_cloud_location_result loc_result = { + .type = LOCATION_TYPE_MULTI_CELL, + .lat = 60.1699, + .lon = 24.9384, + .unc = 200, + }; + + helper_xnrfcloud_connect_ok(); + clear_captured_response(); + + __cmock_nrf_cloud_coap_location_get_ExpectAnyArgsAndReturn(0); + __cmock_nrf_cloud_coap_location_get_ReturnThruPtr_result(&loc_result); + + send_at_command( + "AT#XNRFCLOUDPOS=8,1," + "\"C0:FF:EE:00:11:22\",-55," + "\"DE:AD:BE:EF:CA:FE\",-70\r\n"); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL(strstr(resp, "OK")); + + /* NCELLMEAS notifications */ + k_sleep(K_MSEC(1)); + at_monitor_dispatch(ncellmeas_resp_pci1); + k_sleep(K_MSEC(1)); + at_monitor_dispatch(ncellmeas_resp_gci5); + k_sleep(K_MSEC(1)); + at_monitor_dispatch(ncellmeas_resp_gci5); + k_sleep(K_MSEC(1)); + + resp = get_captured_response(); + TEST_ASSERT_NOT_NULL_MESSAGE(strstr(resp, "#XNRFCLOUDPOS:"), + "Expected #XNRFCLOUDPOS URC in response"); + /* type=1 (LOCATION_TYPE_MULTI_CELL) */ + TEST_ASSERT_NOT_NULL(strstr(resp, "1,")); + + clear_captured_response(); + helper_xnrfcloud_disconnect_ok(); +} + +/* --------------------------------------------------------------------------- + * Main and sys init + * --------------------------------------------------------------------------- + */ + +/* This is needed because AT Monitor library is initialized in SYS_INIT. */ +static int test_at_nrfcloud_sys_init(void) +{ + __cmock_nrf_modem_at_notif_handler_set_ExpectAnyArgsAndReturn(0); + + return 0; +} + +SYS_INIT(test_at_nrfcloud_sys_init, POST_KERNEL, 0); + +extern int unity_main(void); + +int main(void) +{ + (void)unity_main(); + return 0; +} diff --git a/app/tests/at_nrfcloud/testcase.yaml b/app/tests/at_nrfcloud/testcase.yaml new file mode 100644 index 00000000..7c48a2b8 --- /dev/null +++ b/app/tests/at_nrfcloud/testcase.yaml @@ -0,0 +1,7 @@ +tests: + serial_modem.unit_test.at_nrfcloud: + sysbuild: true + platform_allow: + - native_sim + integration_platforms: + - native_sim