nRF91 Device Simulator v2
This is an AWS IoT Thing simulator for nRF91. It shows how to use the Device API for creating JITP certs, associating a newly provisioned device with your tenant (account), doing Firmware Over-the-Air Updates (FOTA), and more.
Usage
Most basic usage
The most basic usage is just creating a device. For that you just need an API key. The device ID will be randomly generated and the rest of the necessary information (mqtt endpoint, cert, and mqtt prefix) will be pulled from the device API.
This will create a new device with AWS IoT, but it will not onboard it to your account (use the -a preconnect
flag) for that.
npx @nrfcloud/device-simulator-v2 -k <api key> [-d <desired device ID>] -t atv2
If you would like to use the local code instead of npx, first run yarn && yarn build
and then replace npx @nrfcloud/device-simulator-v2
with node dist/cli.js
.
You can name the device whatever you want with the -d
option. If not present, it will be named nrfsim-<random 21 digits>
.
Onboard a device to your account
This example will onboard a device to nRF Cloud by creating a new simulated device and associating it to the user's team. The user's API key authenticates the REST request. A default device shadow will be created in the style of the Asset Tracker v2 (atv2) sample firmware. For the pre-connect mode of onboarding, the device simulator will create and provide self-signed certificates in the onboarding process.
npx @nrfcloud/device-simulator-v2 -k <api key> -t atv2 -a preconnect
Simulate sensor outputs
After creating and onboarding your device, you can include any combination of the options listed after -s
below, to simulate the device sending messages containing device sensor data:
npx @nrfcloud/device-simulator-v2 -k <api key> -d <device ID you already onboarded> -s gps,acc,device,temp
Note! Including device
in the list will generate a LOT of device info messages which may overrun your Web browser. Best not to run it more than a few seconds with it, or you can leave out device
and run it long-term.
Note! The acc
option sends FLIP accelerometer messages, simulating the device being flipped right-side-up or upside-down. This was a feature of the Asset Tracker v1 firmware example that has been removed for the currently supported Asset Tracker v2.
If you want to use different GPS data, replace the appropriate file in ./data/sensors or change tne appropriate file path(s) in cli.ts. (There is some additional GPS data in this repo for routes around Portland, Oregon, USA.)
GPS data is based on NMEA sentences. If you want to make your own GPS data, go to https://nmeagen.org. The "Multi-point line" seems to work best. Lay some points and then click the "Generate NMEA file" button.
Do it all at once
You can create a new simulated device, onboard it, and start sending sensor data all in one command, which is a typical way to use the simulator:
npx @nrfcloud/device-simulator-v2 -k <api key> [-d <desired device ID>] -a -s gps,acc,device,temp
Options
These are the options. Most of them are set with environment variables.
-k, --api-key <apiKey> (required) API key for nRF Cloud (default: "")
-h, --api-host <apiHost> API host for nRF Cloud (default: "https://api.nrfcloud.com")
-d, --device-id <deviceId> ID of the device (default: <nrfsim-randomString>)
-a, --onboard <onboardingType> Onboard the device with "jitp", or "preconnect"
-s, --services <services> Comma-delimited list of services to enable. Any of: [gps,acc,temp,device,rsrp,location,log,alert]
-f, --app-fw-version <appFwVersion> Version of the app firmware (default: 1)
-c, --certs-response <certsResponse> JSON returned by call POST /devices/{deviceid}/certificates (default: "")
-e, --endpoint <endpoint> AWS IoT MQTT endpoint (default: "")
-o, --device-ownership-code <deviceOwnershipCode> PIN/ownership code of the device (default: "123456")
-m, --mqtt-messages-prefix <mqttMessagesPrefix> The prefix for the MQTT for this tenant for sending and receiving device messages (default: "")
-v, --verbose Output debug information
-t, --app-type <appType> Specifies the shadow to use. For custom shadow, pass a JSON-encoded shadow object or relative path to json file. Otherwise,
pass "mss" or "atv2" to automatically generate a conformal shadow
-p, --job-execution-path <jobExecutionPath> Specifies an unhappy job execution path for a fota update. View the "Use an unhappy path for FOTA execution" section
of the README for more details.
-h, --help Output usage information
Use npx @nrfcloud/device-simulator-v2 --help
to see the most recent list of options.
Caching
The device simulator makes use of a local cache for things like device certs. This is an effort to minimize the requests made to the API. Clearing the cache is easy.
From the directory in which you ran the simulator:
rm -rf .ez-cache
Or, if actively working on the repo:
yarn clean
Note this is geared towards a Unix/Linux environment. Adjust commands accordingly for Windows.
Contributing
yarn
https://stedolan.github.io/jq/download/
yarn build
node dist/cli.js <options>
Publishing
To publish a new version to npm, follow this recipe:
git add .
git commit -am "<feat|bug|chore|refactor>: <commit message>"
yarn deploy[:beta]
git tag v2.2.7
git push origin HEAD
git push origin v2.2.7
Recipes
See cli.ts for the options. Most of these are set with environment variables.
Set up your environment and provision a device
- Log in to nrfcloud.com and go to the accounts page and grab your API key.
- Install AWS CLI
- If running this on your own AWS account, ensure that Event-based Messages for jobs are enabled in AWS IoT Settings.
- Set up your environment:
export API_KEY=<your_api_key>
export API_HOST=<your_api_host, e.g., https://api.nrfcloud.com>
export AWS_REGION=us-east-1
export DEVICE_RAND=$(node -e 'process.stdout.write(Math.floor(1000000000 + Math.random() * 9000000000).toString())')
export DEVICE_ID=nrfsim-$DEVICE_RAND
export DEVICE_OWNERSHIP_CODE=123456
export CERTS_RESPONSE=$(curl -X POST $API_HOST/v1/devices/$DEVICE_ID/certificates -d "$DEVICE_OWNERSHIP_CODE" -H "Authorization: Bearer $API_KEY" -H "Content-Type: text/plain")
export MQTT_ENDPOINT=$(curl $API_HOST/v1/account -H "Authorization: Bearer $API_KEY" | jq -r .mqttEndpoint)
- Run the simulator, which will just-in-time provision (JITP) the device on nRFCloud (NOTE: JITP can take 20-30 seconds, so be patient...):
node dist/cli.js
You should see some JSON output, with something like this at the end:
************** CONFIG ***********
DEVICE ID: <your_device_id>
DEVICE PIN: 123456
API HOST: https://api.nrfcloud.com
API KEY: <your_api_key>
TENANT ID: <your_tenant_id>
STAGE: prod
*********************************
starting simulator...
connecting to "mqtt.nrfcloud.com"...
connected
Initializing nrfsim-201004647211712200000 shadow...
************** MESSAGE SENT ***********
TOPIC: $aws/things/nrfsim-1065144894/shadow/update
MESSAGE: {
"state": {
"reported": {
"connection": {
"status": "connected",
"keepalive": 30
},
"control": {
"alertsEn": true,
"logLvl": 3
},
"config": {
"activeMode": true,
"locationTimeout": 300,
"activeWaitTime": 300,
"movementResolution": 120,
"movementTimeout": 3600,
"accThreshAct": 4,
"accThreshInact": 4,
"accTimeoutInact": 60,
"nod": []
},
"device": {
"deviceInfo": {
"appVersion": "1",
"batteryVoltage": 5191,
"appName": "asset_tracker_v2",
"imei": "358299840010349",
"board": "nrf9161dk_nrf9161",
"sdkVer": "v2.6.0-571-gf927cd6b1473",
"zephyrVer": "v3.5.99-ncs1-4957-g54b4e400ed8f",
"hwVer": "nRF9161 LACA ADA"
},
"networkInfo": {
"supportedBands": "",
"networkMode": "LTE-M",
"ipAddress": "10.160.33.51",
"ueMode": 2,
"rsrp": -58
},
"simInfo": {
"uiccMode": 1,
"iccid": "",
"imsi": "204080813516718"
},
"serviceInfo": {
"fota_v2": [
"BOOT",
"MODEM",
"APP"
]
},
"connectionInfo": {
"protocol": "MQTT",
"method": "LTE"
}
}
}
}
}
***************************************
Cannot initialize jobs listener until the device "<your_device_id>" is onboarded to your account. You can onboard the device by running "npx @nrfcloud/device-simulator-v2 -k <api key> -d <your_device_id> -a preconnect".
This indicates that the device provisioned with AWS and updated its shadow.
Associate the device with your account (tenant)
- Shut down the script (CMD or CTRL + C).
- Call the
association
endpoint:
curl -X PUT $API_HOST/v1/association/$DEVICE_ID -d "$DEVICE_OWNERSHIP_CODE" -H "Authorization: Bearer $API_KEY" -H "Content-Type: text/plain" | jq
- View your device:
curl $API_HOST/v1/devices/$DEVICE_ID -H "Authorization: Bearer $API_KEY" | jq
If you get a 404
error, try again. It can take a few seconds for the database to record the new device association.
- Restart the simulator:
node dist/cli.js
You should see this JSON output:
confirmed that "<your_device_id>" has been onboarded with account "<your_tenant_id>"!
listening for new jobs...
************** MESSAGE SENT ***********
TOPIC: dev/<your_tenant_id>/<your_device_id>/jobs/req
MESSAGE: [
""
]
***************************************
subscribed to "dev/<your_tenant_id>/<your_device_id>/jobs/rcv"
Your device is now onboarded with your account (tenant) and is ready to start sending and receiving device messages! It is also listening for new FOTA jobs.
Create a new Firmware Over-the-Air (FOTA) job
- Open a new terminal window/tab.
- Set up the environment variables (see above, but use the same
DEVICE_ID
that you had generated). - Upload a dummy firmware file as binary data.
[!NOTE]
If you don't use the -t (atv2,mss) you won't be able to create an update.
curl -X POST $API_HOST/v1/firmwares -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/zip" --data-binary @data/fota_app_fw.zip | jq
Note that you can also upload your firmware as a base64-encoded string:
export DATA_DIR=<absolute_path_to_the_data_folder>
export FILE=$(base64 $DATA_DIR/fota_app_fw.zip)
curl -X POST $API_HOST/v1/firmwares -H "Authorization: Bearer $API_KEY" -H "Content-Type: text/plain" -d $FILE | jq
- Set the
BUNDLE_ID
variable by calling the firmwares
endpoint:
export BUNDLE_ID=$(curl $API_HOST/v1/firmwares -H "Authorization: Bearer $API_KEY" | jq -r '.items[0].bundleId')
- Enable the "APP" type of FOTA on the device (if not already enabled). (The other two types are "BOOT" and "MODEM", and you can set one or all of these in the array. However, uploading modem firmware is not allowed because this is controlled by Nordic Semiconductor personnel.) First you need to find the latest version of your device:
export DEVICE_VERSION=$(curl $API_HOST/v1/devices/$DEVICE_ID -H "Authorization: Bearer $API_KEY" | jq -r '.["$meta"].version')
You can now use this to set an If-Match header, which is required to prevent "lost updates":
curl -X PATCH $API_HOST/v1/devices/$DEVICE_ID/state -d '{ "reported": { "device": { "serviceInfo": { "fota_v2": ["APP"] } } } }' -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -H "If-Match: $DEVICE_VERSION"
- Create the FOTA job
export JOB_ID=$(curl -X POST $API_HOST/v1/fota-jobs -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -d '{ "deviceIdentifiers": ["'$DEVICE_ID'"], "bundleId": "'$BUNDLE_ID'" }' | jq -r '.jobId')
- View your FOTA job
curl $API_HOST/v1/fota-jobs/$JOB_ID -H "Authorization: Bearer $API_KEY" | jq
- Verify the job succeeded in the other tab where you ran
node dist/cli.js
. You should see a series of messages that walk through the firmware installation lifecycle:
************** <your_device_id> ***********
JOB ID: <your_job_id>
OLD STATUS: IN_PROGRESS (1)
NEW STATUS: SUCCEEDED (3)
MESSAGE: installation successful for "APP" firmware file from "<your_bundle_id_url>"
********************************************
If successful, you should see a message like this:
job "<your_job_id>" succeeded!
If you do not see this it's possible that a previously created job has not succeeded. This will block any newly created jobs from running. You can check this by using the GET /fota-jobs
endpoint (as you did above) and then using DELETE $API_HOST/v1/fota-jobs/<your-jobId>
for any previously created jobs that has a status other than SUCCEEDED
.
- You can also verify the job succeeded by using the Device API:
curl $API_HOST/v1/fota-job-execution-statuses/$JOB_ID -H "Authorization: Bearer $API_KEY" | jq
or
curl $API_HOST/v1/fota-job-executions/$DEVICE_ID/$JOB_ID -H "Authorization: Bearer $API_KEY" | jq
Use an unhappy path for FOTA execution
If you want to test devices failing, stalling, or timing out you can add the -p
flag with one of the following options:
Value | Execution Path | Description |
---|
0 | QUEUED | Simulates a device not being turned on |
1 | QUEUED --> REJECTED | Simulates a device instantly rejecting a job |
2 | QUEUED --> DOWNLOADING | Simulates a device hanging in the DOWNLOADING state |
3 | QUEUED --> DOWNLOADING --> IN_PROGRESS | Simulates a device attempting to update, but is unable to respond |
4 | QUEUED --> DOWNLOADING --> IN_PROGRESS --> TIMED_OUT | Simulates a device timing out while processing the update |
5 | QUEUED --> DOWNLOADING --> IN_PROGRESS --> FAILED | Simulates a device failing to apply the update and keeping the existing firmware installed |
Clean up (if desired)
curl -X DELETE $API_HOST/v1/fota-jobs/<your-jobId> -H "Authorization: Bearer $API_KEY"
curl -X DELETE $API_HOST/v1/firmwares/$BUNDLE_ID -H "Authorization: Bearer $API_KEY"
curl -X DELETE $API_HOST/v1/devices/$DEVICE_ID -d $DEVICE_OWNERSHIP_CODE -H "Authorization: Bearer $API_KEY" -H "Content-Type: text/plain"