AWS IoT Thing simulator for nRF91

Usage no npm install needed!

<script type="module">
  import nrfcloudDeviceSimulatorV2 from 'https://cdn.skypack.dev/@nrfcloud/device-simulator-v2';


nRF91 Device Simulator v2 npm version

Build Status semantic-release Commitizen friendly

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.


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.

The host defaults to the production environment (https://api.nrfcloud.com). Nordic Semiconductor personnel: if you want to use the simulator on our dev, feature, or beta environments, set a API_HOST env var to https://api.[dev|feature|beta].nrfcloud.com (you will need an AWS account for that environment). You can also set the API_HOST env var to the url of your sub account (ie https://api.<user_id>.nrfcloud.com) and the stage will be automatically set to dev (the stage is used for the job subscription topics for FOTA). You no longer need to set a STAGE variable, and it will be ignored.

This will create a new device with AWS IoT, but it will not associate it to your account (use the -a flag) for that.

npx @nrfcloud/device-simulator-v2 -k <api key>

(If you would like to use the local code instead of npx, first run yarn build and then replace npx @nrfcloud/device-simulator-v2 with node dist/cli.js.)

Associate device to your account

This will create a new device and associate it to the account for the API key.

npx @nrfcloud/device-simulator-v2 -k <api key> -a

Simulate sensor outputs

npx @nrfcloud/device-simulator-v2 -k <api key> -d <device id (from output of initial command that associated device)> -s gps,acc,device,temp


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, --associate                                    Automatically associate device to your account (default: false)
  -s, --services <services>                          Comma-delimited list of services to enable. Any of: [gps,acc,temp,device]
  -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
  -h, --help                                         Output usage information

Use npx @nrfcloud/device-simulator-v2 --help to see the most recent list of options.


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 *nix environment. Adjust commands accordingly for Windows.


# install deps

# install jq

# modify your files

# compile
yarn build

# test
node dist/cli.js <options>


To publish a new version to npm, follow this recipe:

# commit all files
git commit -am "<feat|bug|chore|refactor>: <commit message>"

# publish to npm. this will build, ask for a new version, and publish 
# to the npm repo if you run `yarn deploy:beta, it will deploy to the beta tag
yarn deploy[:beta]

# push changes and new tags to github
git push origin HEAD


See cli.ts for the options. Most of these are set with environment variables.

Set up your environment and provision a device

  1. Log in to nrfcloud.com and go to the accounts page and grab your API key.
  2. Install AWS CLI
  3. If running this on your own AWS account, ensure that Event-based Messages for jobs are enabled in AWS IoT Settings.
  4. 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 #if your region is different, change it here
export DEVICE_RAND=$(node -e 'process.stdout.write(Math.floor(1000000000 + Math.random() * 9000000000).toString())')
export DEVICE_ID=nrfsim-$DEVICE_RAND

# create device certificates
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)
  1. Run the simulator, which will just-in-time provision (JITP) the device on nRFCloud (NOTE: JITP can take 20-30 seconds, so be patient...):
# don't forget to "yarn build" to generate the dist dir
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.<stage.>nrfcloud.com
API KEY: <your_api_key>
TENANT ID: <your_tenant_id>
STAGE: <stage>

starting simulator...

connecting to "a2wg6q8yw7gv5r-ats.iot.us-east-1.amazonaws.com"...


Initializing nrfsim-201004647211712200000 shadow...

************** MESSAGE SENT ***********
TOPIC: $aws/things/nrfsim-1065144894/shadow/update
  "state": {
    "reported": {
      "device": {
        "serviceInfo": {
          "fota_v2": [
          "ui": [
        "networkInfo": {
          "currentBand": 12,
          "supportedBands": "",
          "areaCode": 36874,
          "mccmnc": "310410",
          "ipAddress": "",
          "ueMode": 2,
          "cellID": 84485647,
          "networkMode": "LTE-M GPS"
        "simInfo": {
          "uiccMode": 1,
          "iccid": "",
          "imsi": "204080813516718"
        "deviceInfo": {
          "modemFirmware": "mfw_nrf9160_1.1.0",
          "batteryVoltage": 3824,
          "imei": "352656100441776",
          "board": "nrf9160_pca20035",
          "appVersion": "1",
          "appName": "asset_tracker"
      "connection": {
        "status": "connected",
        "keepalive": 30

Cannot initialize jobs listener until the device "<your_device_id>" is associated to your account. You can associate the device by running "npx @nrfcloud/device-simulator-v2 -k <api key> -d <your_device_id> -a".

This indicates that the device provisioned with AWS and updated its shadow.

Associate the device with your account (tenant)

  1. Shut down the script (CMD or CTRL + C).
  2. 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
  1. 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.

  1. Restart the simulator:
node dist/cli.js

You should see this JSON output:

confirmed that "<your_device_id>" has been associated with account "<your_tenant_id>"!

listening for new jobs...

************** MESSAGE SENT ***********
TOPIC: dev/<your_tenant_id>/<your_device_id>/jobs/req

subscribed to "dev/<your_tenant_id>/<your_device_id>/jobs/rcv"

Your device is now associated 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

  1. Open a new terminal window/tab.
  2. Set up the environment variables (see above, but use the same DEVICE_ID that you had generated).
  3. Upload a dummy firmware file as binary data.
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
  1. 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')
  1. 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"
  1. 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')
  1. View your FOTA job
curl $API_HOST/v1/fota-jobs/$JOB_ID -H "Authorization: Bearer $API_KEY" | jq
  1. 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>
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.

  1. 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


curl $API_HOST/v1/fota-job-executions/$DEVICE_ID/$JOB_ID -H "Authorization: Bearer $API_KEY" | jq

Enable Sensors and Services

  1. Shut down the script (CMD or CTRL + C).
  2. Restart the simulator with the GPS service enabled:
node dist/cli.js -s gps

Or restart the simulator with all the services enabled:

node dist/cli.js -s gps,acc,device,temp

If you want to use different data simply 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.)

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.

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"