AWS IoT Core to LOCUS Integration
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.
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.
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.
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
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.
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.
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.
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.
Console path:
API Gateway → Create API → HTTP API → Build
| Setting | Value |
|---|---|
| API name | locus_downlink_api |
| Integration | Add integration → Lambda |
| Lambda function | dl_receiver |
| API endpoint type | Regional |
Click Next.
| Setting | Value |
|---|---|
| Method | POST |
| Resource path | /downlink |
| Integration target | dl_receiver |
Click Next.
| Setting | Value |
|---|---|
| Stage name | $default |
| Auto-deploy | Enabled |
Click Next.
- Ensure Lambda function
dl_receiveris properly selected - Route
/downlinkwithPOSTmethod is configured - Stage
$defaulthas auto-deploy enabled
Click Create.
Retrieve the API endpoint URL:
After the HTTP API is successfully created:
- Open
locus_downlink_api - Find Invoke URL under Stages →
$default(e.g.,https://abc123.execute-api.us-east-1.amazonaws.com) - 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.
| 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 |