Skip to content

Commit 554fb6e

Browse files
committed
feat: add nRF Cloud provisioning support
Add support for nRF Cloud provisioning: - Modify the cloud module to include states for provisioning. - Add support for reprovisioning of the device via the device shadow. The device will initiate reprovisioning if receiving provisioning commands via the device shadow. This pipeline can be used to issue other commands as well. Reboot also added as an example command. If the device has no valid credentials after boot, it is considered unclaimed. The user needs to claim the device using the attestation token. The device will back off using the cloud built-in backoff mechanism until it's claimed and provisioning can start. If the device is claimed and authenticated, it will start the connection immediately as before. - Update main CDDL format to be able to parse the new shadow format. - Add on-target test that tests different provisioning scenarios on target. Documentation on how to use the feature will follow. Signed-off-by: Simen S. Røstad <simen.rostad@nordicsemi.no>
1 parent 643e6ef commit 554fb6e

File tree

28 files changed

+1105
-212
lines changed

28 files changed

+1105
-212
lines changed

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ More information on how to customize the template can be found in the [Customiza
8080

8181
## Quick Start
8282

83-
This section provides a brief introduction to building and running the Asset Tracker Template on supported hardware. For detailed instructions, refer to the [Getting Started](docs/common/getting_started.md) document.
83+
This section provides a brief introduction to building and running the Asset Tracker Template on supported hardware.
84+
For detailed setup including nRF Cloud provisioning and advanced build configurations, see [Getting Started](docs/common/getting_started.md).
8485

8586
### Prerequisites
8687

@@ -109,7 +110,11 @@ west thingy91x-dfu # For Thingy:91 X serial bootloader
109110
west flash --erase
110111
```
111112

112-
For detailed setup including nRF Cloud provisioning and advanced build configurations, see [Getting Started](docs/common/getting_started.md).
113+
3. Provision the device:
114+
115+
Run the [nRF Connect for Desktop Quickstart application](https://docs.nordicsemi.com/bundle/nrf-connect-desktop/page/index.html) to provision the device and connect to nRF Cloud over CoaP.
116+
117+
For detailed provisioning instructions, see the [Provisioning](docs/common/provisioning.md) documentation.
113118

114119
## Further reading
115120

app/Kconfig.sysbuild

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@ endchoice
2121
config PM_EXTERNAL_FLASH_MCUBOOT_SECONDARY
2222
default y if BOARD_NRF9151DK_NRF9151_NS
2323

24+
config MCUBOOT_USE_ALL_AVAILABLE_RAM
25+
default y
26+
2427
source "${ZEPHYR_BASE}/share/sysbuild/Kconfig"

app/boards/thingy91x_nrf9151_ns.conf

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ CONFIG_NET_STATISTICS_WIFI=y
3636
# Heap allocations should be changed when CONFIG_LOCATION_METHOD_WIFI_SCANNING_RESULTS_MAX_CNT
3737
# and CONFIG_NRF_WIFI_SCAN_MAX_BSS_CNT (which should be the same value) are changed.
3838
CONFIG_NRF_WIFI_CTRL_HEAP_SIZE=10240
39-
CONFIG_HEAP_MEM_POOL_SIZE=6144
40-
CONFIG_HEAP_MEM_POOL_IGNORE_MIN=y
4139

4240
# Wi-Fi location
4341
CONFIG_LOCATION_METHOD_WIFI=y

app/prj.conf

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ CONFIG_TFM_SPM_LOG_LEVEL_DEBUG=y
2525
CONFIG_MAIN_STACK_SIZE=2560
2626
# Extended AT host/monitor stack/heap sizes since some nrf_cloud credentials are longer than 1024 bytes.
2727
CONFIG_AT_MONITOR_HEAP_SIZE=2176
28-
CONFIG_HEAP_MEM_POOL_SIZE=10000
28+
CONFIG_HEAP_MEM_POOL_SIZE=12288
2929
CONFIG_HEAP_MEM_POOL_IGNORE_MIN=y
3030
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=1280
3131

@@ -230,3 +230,17 @@ CONFIG_TASK_WDT_MIN_TIMEOUT=10000
230230
# Device power management
231231
CONFIG_PM_DEVICE=y
232232
CONFIG_PM_DEVICE_SHELL=y
233+
234+
# nRF Cloud Provisioning
235+
CONFIG_MODEM_ATTEST_TOKEN=y
236+
CONFIG_NRF_PROVISIONING=y
237+
CONFIG_NRF_PROVISIONING_LOG_LEVEL_DBG=y
238+
CONFIG_NRF_PROVISIONING_CBOR_RECORDS=5
239+
CONFIG_NRF_PROVISIONING_COAP=y
240+
CONFIG_NRF_PROVISIONING_CODEC=y
241+
CONFIG_NRF_PROVISIONING_CBOR=y
242+
CONFIG_NRF_PROVISIONING_WITH_CERT=y
243+
CONFIG_NRF_PROVISIONING_RX_BUF_SZ=4096
244+
CONFIG_NRF_PROVISIONING_TX_BUF_SZ=4096
245+
CONFIG_NRF_PROVISIONING_CODEC_AT_CMD_LEN=2048
246+
CONFIG_NRF_PROVISIONING_CODEC_RX_SZ_START=2048

app/src/cbor/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ set(zcbor_command
1313
--cddl ${CMAKE_CURRENT_SOURCE_DIR}/device_shadow.cddl
1414
--decode # Generate decoding functions
1515
--short-names # Attempt to make generated symbol names shorter (at the risk of collision)
16-
-t config-object # Create a public API for decoding the "config-object" type from the cddl file
16+
-t desired-object # Create a public API for decoding the "desired-object" type from the cddl file
1717
--output-cmake device_shadow.cmake # The generated cmake file will be placed here
1818
)
1919
execute_process(COMMAND ${zcbor_command}

app/src/cbor/cbor_helper.c

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,47 @@
44
* SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
55
*/
66
#include <errno.h>
7+
#include <string.h>
8+
9+
#include "cbor_helper.h"
710
#include "device_shadow_decode.h"
811

9-
int get_update_interval_from_cbor_response(const uint8_t *cbor,
10-
size_t len,
11-
uint32_t *interval_sec)
12+
#include <zephyr/logging/log.h>
13+
14+
LOG_MODULE_REGISTER(cbor_helper, CONFIG_APP_LOG_LEVEL);
15+
16+
int get_parameters_from_cbor_response(const uint8_t *cbor,
17+
size_t len,
18+
uint32_t *interval_sec,
19+
uint32_t *command_type)
1220
{
13-
struct config_object object = { 0 };
14-
size_t not_used;
21+
int err;
22+
struct desired_object desired_object = { 0 };
23+
size_t decode_len = len;
1524

16-
int err = cbor_decode_config_object(cbor, len, &object, &not_used);
25+
if (!cbor || !interval_sec || !command_type || len == 0) {
26+
return -EINVAL;
27+
}
1728

29+
err = cbor_decode_desired_object(cbor, decode_len, &desired_object, &decode_len);
1830
if (err) {
31+
LOG_ERR("cbor_decode_desired_object, error: %d", err);
32+
LOG_HEXDUMP_ERR(cbor, len, "Unexpected CBOR data");
1933
return -EFAULT;
2034
}
2135

22-
*interval_sec = object.update_interval;
36+
if (desired_object.config_present && desired_object.config.update_interval_present) {
37+
*interval_sec = desired_object.config.update_interval.update_interval;
38+
} else {
39+
LOG_DBG("Update interval parameter not present");
40+
}
41+
42+
if (desired_object.command_present) {
43+
*command_type = desired_object.command.type;
44+
/* ID entry not used */
45+
} else {
46+
LOG_DBG("Command parameter not present");
47+
}
2348

2449
return 0;
2550
}

app/src/cbor/cbor_helper.h

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,23 @@
66

77
#include <zephyr/types.h>
88

9+
#define CLOUD_COMMAND_TYPE_PROVISION 1
10+
#define CLOUD_COMMAND_TYPE_REBOOT 2
11+
912
/**
10-
* @brief Get the update interval from a CBOR buffer.
13+
* @brief Get the device shadow parameters from a CBOR buffer.
1114
*
1215
* @param[in] cbor The CBOR buffer.
1316
* @param[in] len The length of the CBOR buffer.
14-
* @param[out] interval_sec The update interval in seconds.
17+
* @param[out] interval_sec Update interval in seconds.
18+
* @param[out] command_type Cloud command type.
1519
*
1620
* @returns 0 If the operation was successful.
1721
* Otherwise, a (negative) error code is returned.
1822
* @retval -EFAULT if the CBOR buffer is invalid.
1923
*
2024
*/
21-
int get_update_interval_from_cbor_response(const uint8_t *cbor,
22-
size_t len,
23-
uint32_t *interval_sec);
25+
int get_parameters_from_cbor_response(const uint8_t *cbor,
26+
size_t len,
27+
uint32_t *interval_sec,
28+
uint32_t *command_type);

app/src/cbor/device_shadow.cddl

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
1-
config-object = {
2-
"config": {
3-
"update_interval": uint .size 4
4-
}
1+
; Device Shadow Desired Object Definition
2+
; ---------------------------------------
3+
; This section defines the structure of the "desired" object in the device shadow.
4+
; All sections are optional, but if present, they must appear in the specified order when decoding the CBOR buffer.
5+
;
6+
; Sections:
7+
; 1. "config": Contains device configuration parameters.
8+
; - "update_interval": (optional) A 4-byte unsigned integer specifying the update interval.
9+
; - Additional configuration parameters may be included as key-value pairs (tstr => any).
10+
;
11+
; 2. "command": (optional) An array specifying a command for the device to execute.
12+
; - Structure: [type, id]
13+
; - type: 4-byte unsigned integer indicating the command type.
14+
; - id: 4-byte unsigned integer indicating the command ID.
15+
;
16+
; Any additional fields may be included as key-value pairs (tstr => any).
17+
18+
desired-object = {
19+
? "config": {
20+
? "update_interval": uint .size 4,
21+
* tstr => any
22+
},
23+
? "command": [type: uint .size 4, id: uint .size 4],
24+
* tstr => any
525
}

app/src/main.c

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,73 @@ static void timer_work_fn(struct k_work *work)
335335
}
336336
}
337337

338+
static void on_shadow_response(struct main_state *state_object, const struct cloud_msg *msg)
339+
{
340+
int err;
341+
uint32_t command_type = UINT32_MAX;
342+
uint32_t interval_sec = UINT32_MAX;
343+
344+
err = get_parameters_from_cbor_response(msg->response.buffer,
345+
msg->response.buffer_data_len,
346+
&interval_sec,
347+
&command_type);
348+
if (err) {
349+
LOG_ERR("get_parameters_from_cbor_response, error: %d", err);
350+
return;
351+
}
352+
353+
/* Set new interval if valid */
354+
if (interval_sec != UINT32_MAX) {
355+
state_object->interval_sec = interval_sec;
356+
357+
LOG_DBG("Received new interval: %d seconds", state_object->interval_sec);
358+
359+
err = k_work_reschedule(&trigger_work, K_SECONDS(state_object->interval_sec));
360+
if (err < 0) {
361+
LOG_ERR("k_work_reschedule, error: %d", err);
362+
SEND_FATAL_ERROR();
363+
return;
364+
}
365+
}
366+
367+
/* For commands, only process delta responses. This avoids executing commands that may still
368+
* persist in the shadow's desired section. Delta responses only include new
369+
* commands, since the device clears received commands by reporting them in
370+
* the reported section of the shadow.
371+
*/
372+
if ((msg->type != CLOUD_SHADOW_RESPONSE_DELTA) && (command_type != UINT32_MAX)) {
373+
return;
374+
}
375+
376+
LOG_DBG("Received command ID: %d, translated to %s", command_type,
377+
(command_type == CLOUD_COMMAND_TYPE_PROVISION) ?
378+
"CLOUD_COMMAND_TYPE_PROVISION" :
379+
(command_type == CLOUD_COMMAND_TYPE_REBOOT) ?
380+
"CLOUD_COMMAND_TYPE_REBOOT" : "UNKNOWN");
381+
382+
/* Check for known commands */
383+
if (command_type == CLOUD_COMMAND_TYPE_PROVISION) {
384+
385+
struct cloud_msg cloud_msg = {
386+
.type = CLOUD_PROVISIONING_REQUEST,
387+
};
388+
389+
err = zbus_chan_pub(&CLOUD_CHAN, &cloud_msg, K_SECONDS(1));
390+
if (err) {
391+
LOG_ERR("zbus_chan_pub, error: %d", err);
392+
SEND_FATAL_ERROR();
393+
return;
394+
}
395+
} else if (command_type == CLOUD_COMMAND_TYPE_REBOOT) {
396+
LOG_WRN("Received command ID: %d", command_type);
397+
398+
/* Flush the log buffer and wait a few seconds before rebooting */
399+
LOG_PANIC();
400+
k_sleep(K_SECONDS(10));
401+
sys_reboot(SYS_REBOOT_COLD);
402+
}
403+
}
404+
338405
/* Zephyr State Machine framework handlers */
339406

340407
/* STATE_RUNNING */
@@ -440,29 +507,34 @@ static void triggering_run(void *o)
440507
if (state_object->chan == &CLOUD_CHAN) {
441508
const struct cloud_msg *msg = MSG_TO_CLOUD_MSG_PTR(state_object->msg_buf);
442509

443-
if (msg->type == CLOUD_DISCONNECTED) {
510+
switch (msg->type) {
511+
case CLOUD_DISCONNECTED:
444512
state_object->connected = false;
445-
smf_set_state(SMF_CTX(state_object), &states[STATE_IDLE]);
446-
return;
447-
}
448513

449-
if (msg->type == CLOUD_SHADOW_RESPONSE) {
450-
err = get_update_interval_from_cbor_response(msg->response.buffer,
451-
msg->response.buffer_data_len,
452-
&state_object->interval_sec);
514+
/* Because the location library in the location module performs its own
515+
* calls to cloud we cancel any ongoing location request when the cloud
516+
* connection is lost. This is to prevent the location module from
517+
* attempting to send location data to the cloud when it is not connected.
518+
*/
519+
enum location_msg_type location_msg = LOCATION_SEARCH_CANCEL;
520+
521+
err = zbus_chan_pub(&LOCATION_CHAN, &location_msg, K_SECONDS(1));
453522
if (err) {
454-
LOG_ERR("get_update_interval_from_cbor_response, error: %d", err);
523+
LOG_ERR("zbus_chan_pub, error: %d", err);
524+
SEND_FATAL_ERROR();
455525
return;
456526
}
457527

458-
LOG_WRN("Received new interval: %d seconds", state_object->interval_sec);
459-
460-
err = k_work_reschedule(&trigger_work,
461-
K_SECONDS(state_object->interval_sec));
462-
if (err < 0) {
463-
LOG_ERR("k_work_reschedule, error: %d", err);
464-
SEND_FATAL_ERROR();
465-
}
528+
smf_set_state(SMF_CTX(state_object), &states[STATE_IDLE]);
529+
return;
530+
case CLOUD_SHADOW_RESPONSE_DELTA:
531+
__fallthrough;
532+
case CLOUD_SHADOW_RESPONSE_DESIRED:
533+
on_shadow_response(state_object, msg);
534+
return;
535+
default:
536+
/* Don't care */
537+
break;
466538
}
467539
}
468540
}

0 commit comments

Comments
 (0)