Documentation
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

AWS IoT Core to LOCUS Integration

LOCUS Integration with Third‑Party Network Server (AWS IoT Core)

Table of Contents


This guide explains how to configure AWS IoT Core for LoRaWAN to:

  • Uplink (UL): Automatically forward messages from LoRaWAN devices to the LOCUS application.
  • Downlink (DL): Receive downlink commands from LOCUS and deliver them to LoRaWAN devices.

Note: You can skip Part 2 (Downlink Configuration) if one of the following is true:

  • You will not use the Device Configuration feature on LOCUS, OR
  • You will not use STORK or CHICKADEE devices with GNSS resolving and Wi‑Fi‑based tracking enabled (these features require downlink communication).

The guide walks through creating the necessary IAM roles, Lambda functions, IoT Rule, API Gateway, and Destination within AWS — enabling secure and reliable bidirectional data delivery.


Part 1: Uplink (UL) Configuration

1. Create the UL Lambda function

Console path: AWS Lambda → Create function

Settings:

Setting Value
Create function options Author from scratch
Function name ul_forwarder
Runtime Python 3.13
Architecture x86_64

Lambda code (lambda_function.py):

import os
import base64
import json
import urllib.request
from array import array
from datetime import datetime, timezone

ENDPOINT_URL = os.getenv("ENDPOINT_URL", "")
API_KEY_VALUE = os.getenv("API_KEY_VALUE", "")
HEADERS = {"Content‑Type": "application/json", "ApiKey": API_KEY_VALUE}
TIMEOUT = 5

_GPS_EPOCH = datetime(1980, 1, 6, tzinfo=timezone.utc)
_LEAP_OFFSET = 18  # GPS‑UTC offset

def lambda_handler(event, context):
    lora = event["WirelessMetadata"]["LoRaWAN"]
    ts = int((datetime.fromisoformat(lora["Timestamp"].replace("Z", "+00:00")) - _GPS_EPOCH).total_seconds()) - _LEAP_OFFSET
    signed_bytes = array("b", base64.b64decode(event["PayloadData"])).tolist()
    g_infos = [{
        "gwEui": gw["GatewayEui"].upper(),
        "rssi": gw["Rssi"],
        "snr": round(gw["Snr"], 1)
    } for gw in lora.get("Gateways", [])]
    body = {
        "recvTime": ts,
        "deviceEui": lora["DevEui"].upper(),
        "fPort": lora["FPort"],
        "bytes": signed_bytes,
        "gatewayRxInfo": g_infos,
        "dataRate": int(lora["DataRate"]),
        "fCntUp": lora["FCnt"],
        "uLFreq": lora["Frequency"],
        "adrBit": lora["ADR"],
        "devAddr": lora["DevAddr"].upper()
    }
    req = urllib.request.Request(
        ENDPOINT_URL,
        data=json.dumps(body, separators=(",", ":")).encode(),
        headers=HEADERS,
        method="POST"
    )
    with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
        return {"status": r.status, "body": r.read().decode()}

Add environment variables (Configuration → Environment variables)

Key Value
ENDPOINT_URL https://locus-pc.tektelic.com/v2/integration
API_KEY_VALUE ApiKey

Click Deploy after adding the code and environment variables.


2. Create the IoT Rule

Path: AWS IoT Core → Message routing → Rules → Create rule

Name: ul_to_lambda_rule

SQL statement:

SELECT *

Action: Add action → Invoke Lambda function → choose ul_forwarder

Click Create rule after completing all steps.


3. Allow IoT Core to invoke the Lambda

Run the following command in AWS CloudShell (replace account ID and region):

aws lambda add-permission \
  --function-name ul_forwarder \
  --principal iot.amazonaws.com\
  --source-arn arn:aws:iot:<region>:<account_id>:rule/ul_to_lambda_rule \
  --statement-id iot-invoke \
  --action lambda:InvokeFunction

Also, it is possible to run the command with AWS CLI installed (replace account ID, profile, and region)

aws lambda add-permission \
  --region <region> \
  --profile <profile> \
  --function-name arn:aws:lambda:<region>:<account_id>:function:ul_forwarder \ 
  --principal iot.amazonaws.com \
  --source-arn arn:aws:iot:<region>:<account_id>:rule/ul_to_lambda_rule \
  --source-account <account_id> \
  --statement-id iot-invoke \
  --action lambda:InvokeFunction

4. Create the Destination

Path:
IoT Core → Manage → LPWAN devices → Destinations → Add destination
Destination name: ul_to_lambda_dest
Use AWS IoT rule: select your rule ul_to_lambda_rule

IAM role: choose an existing role (e.g., AWSIotWirelessDestination-<id>) or create a new one named AWSIotWirelessDestination.
Ensure that the role has this policy

{
  "Effect": "Allow",
  "Action": ["iot:DescribeEndpoint", "iot:Publish"],
  "Resource": "*"
}

Click Create destination.


5. Attach Destination to Device

Path:
IoT Core → Manage → LPWAN devices → Devices → [your device] → Edit → Destination → Select ul_to_lambda_dest → Save

Now every uplink from that device triggers the IoT rule → your Lambda → external endpoint.


1. Create the DL Lambda function

Console path:
AWS Lambda → Create function

Setting Value
Create function options Author from scratch
Function name dl_receiver
Runtime Python 3.13
Architecture x86_64

Paste this into the Lambda code editor (lambda_function.py):

import json
import base64
import boto3

iot_wireless = boto3.client('iotwireless')

def lambda_handler(event, context):
    """Receive DL from LOCUS and forward to AWS IoT Wireless"""
    try:
        body = json.loads(event['body'])
        device_eui = body['deviceEui'].lower()
        
        # Get device ID using DevEUI
        device_info = iot_wireless.get_wireless_device(
            Identifier=device_eui,
            IdentifierType='DevEui'
        )
        device_id = device_info['Id']
        
        # Convert signed bytes to unsigned bytes, then to base64
        signed_bytes = body['data']['bytes']
        unsigned_bytes = bytes((b & 0xFF) for b in signed_bytes)
        payload_b64 = base64.b64encode(unsigned_bytes).decode()
        
        # Send downlink via AWS IoT Wireless API
        response = iot_wireless.send_data_to_wireless_device(
            Id=device_id,
            TransmitMode=1 if body.get('confirmed', False) else 0,
            PayloadData=payload_b64,
            WirelessMetadata={
                'LoRaWAN': {
                    'FPort': body['data']['fPort']
                }
            }
        )
        
        return {
            'statusCode': 200,
            'body': json.dumps({
                'messageId': response['MessageId'],
                'msgId': body.get('msgId')
            })
        }
        
    except iot_wireless.exceptions.ResourceNotFoundException:
        return {
            'statusCode': 404,
            'body': json.dumps({'error': f'Device {device_eui} not found'})
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'error': str(e)})
        }

Click Deploy after adding the code.


2. Add IAM Policy to Lambda Execution Role

Console path:
AWS Lambda → dl_receiver → Configuration → Permissions → Execution role → click the role name

In IAM console, click Add permissions → Create inline policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iotwireless:SendDataToWirelessDevice",
        "iotwireless:GetWirelessDevice"
      ],
      "Resource": "*"
    }
  ]
}

Name it IoTWirelessDownlinkPolicy and click Create policy.


3. Create API Gateway HTTP API

Console path: API Gateway → Create API → HTTP API → Build

Step 1: Configure API

Setting Value
API name locus_downlink_api
Integration Add integration → Lambda
Lambda function dl_receiver
API endpoint type Regional

Click Next.

Step 2: Configure routes

Setting Value
Method POST
Resource path /downlink
Integration target dl_receiver

Click Next.

Step 3: Define stages

Setting Value
Stage name $default
Auto-deploy Enabled

Click Next.

Step 4: Review and create

  • Ensure Lambda function dl_receiver is properly selected
  • Route /downlink with POST method is configured
  • Stage $default has auto-deploy enabled

Click Create.

Retrieve the API endpoint URL:

After the HTTP API is successfully created:

  1. Open locus_downlink_api
  2. Find Invoke URL under Stages → $default (e.g., https://abc123.execute-api.us-east-1.amazonaws.com)
  3. Your complete downlink endpoint will be: https://abc123.execute-api.us-east-1.amazonaws.com/downlink

After creating the API Gateway endpoint in AWS, register it with LOCUS so that LOCUS can send downlink messages to your AWS infrastructure.

API endpoint: https://locus-api.tektelic.com/api/ns-credentials

Method: POST

Headers:
- accept: application/json
- Authorization: Bearer <YOUR_LOCUS_TOKEN>
- Content-Type: application/json

Request body:

{
  "tekNsDlUrl": "https://abc123.execute-api.us-east-1.amazonaws.com/downlink",
  "region": null
}

Example using curl:

curl -X 'POST' \
  'https://locus-api.tektelic.com/api/ns-credentials' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer <YOUR_LOCUS_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{
  "tekNsDlUrl": "https://abc123.execute-api.us-east-1.amazonaws.com/downlink",
  "region": null
}'

After successful configuration, LOCUS will use this endpoint to send downlink messages to your LoRaWAN devices through AWS IoT Core.


Summary of AWS components

Component Name Purpose
Lambda (UL) ul_forwarder Sends uplink JSON to LOCUS endpoint
Lambda (DL) dl_receiver Receives DL from LOCUS, converts format, sends to device
Env vars (UL) ENDPOINT_URL, API_KEY_VALUE Configures LOCUS destination + auth
IoT Rule ul_to_lambda_rule Forwards LoRaWAN uplink to Lambda
Destination ul_to_lambda_dest Binds LoRaWAN device → IoT Rule
IAM role (Destination) AWSIotWirelessDestination Allows LNS to publish to IoT Core
API Gateway locus_downlink_api HTTP endpoint for LOCUS downlinks
IAM Policy (DL) IoTWirelessDownlinkPolicy Allows Lambda to get device info and send downlinks