-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
fe04cfc
commit ee6f01b
Showing
16 changed files
with
536 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import { Construct } from 'constructs' | ||
import { | ||
aws_dynamodb as DynamoDB, | ||
RemovalPolicy, | ||
Duration, | ||
aws_iam as IAM, | ||
aws_iot as IoT, | ||
aws_lambda as Lambda, | ||
Stack, | ||
aws_logs as Logs, | ||
} from 'aws-cdk-lib' | ||
import type { PackedLambda } from '../backend' | ||
|
||
/** | ||
* Contains resources that provide LwM2M based data for devices | ||
*/ | ||
export class LwM2M extends Construct { | ||
public readonly table: DynamoDB.ITable | ||
constructor( | ||
parent: Construct, | ||
{ | ||
lambdaSources, | ||
baseLayer, | ||
}: { | ||
lambdaSources: { | ||
updatesToLwM2M: PackedLambda | ||
} | ||
baseLayer: Lambda.ILayerVersion | ||
}, | ||
) { | ||
super(parent, 'LwM2M') | ||
|
||
this.table = new DynamoDB.Table(this, 'table', { | ||
billingMode: DynamoDB.BillingMode.PAY_PER_REQUEST, | ||
partitionKey: { | ||
name: 'id', | ||
type: DynamoDB.AttributeType.STRING, | ||
}, | ||
timeToLiveAttribute: 'ttl', | ||
removalPolicy: RemovalPolicy.DESTROY, | ||
}) | ||
|
||
const fn = new Lambda.Function(this, 'updatesToLwM2M', { | ||
handler: lambdaSources.updatesToLwM2M.handler, | ||
architecture: Lambda.Architecture.ARM_64, | ||
runtime: Lambda.Runtime.NODEJS_18_X, | ||
timeout: Duration.seconds(60), | ||
memorySize: 1792, | ||
code: Lambda.Code.fromAsset(lambdaSources.updatesToLwM2M.lambdaZipFile), | ||
description: | ||
'Invoked when devices report their cell locationStore shadow updates asset_tracker_v2 shadow format as LwM2M objects in a named shadow. Also store the updates in a table for historical data.', | ||
layers: [baseLayer], | ||
environment: { | ||
VERSION: this.node.tryGetContext('version'), | ||
TABLE_NAME: this.table.tableName, | ||
}, | ||
initialPolicy: [ | ||
new IAM.PolicyStatement({ | ||
actions: ['iot:UpdateThingShadow'], | ||
resources: ['*'], | ||
}), | ||
], | ||
logRetention: Logs.RetentionDays.ONE_WEEK, | ||
}) | ||
|
||
this.table.grantWriteData(fn) | ||
|
||
const ruleRole = new IAM.Role(this, 'ruleRole', { | ||
assumedBy: new IAM.ServicePrincipal( | ||
'iot.amazonaws.com', | ||
) as IAM.IPrincipal, | ||
inlinePolicies: { | ||
rootPermissions: new IAM.PolicyDocument({ | ||
statements: [ | ||
new IAM.PolicyStatement({ | ||
actions: ['iot:Publish'], | ||
resources: [ | ||
`arn:aws:iot:${Stack.of(parent).region}:${ | ||
Stack.of(parent).account | ||
}:topic/errors`, | ||
], | ||
}), | ||
], | ||
}), | ||
}, | ||
}) | ||
|
||
const rule = new IoT.CfnTopicRule(this, 'rule', { | ||
topicRulePayload: { | ||
description: `Convert shadow updates to LwM2M`, | ||
ruleDisabled: false, | ||
awsIotSqlVersion: '2016-03-23', | ||
sql: [ | ||
`SELECT * as update,`, | ||
`topic(3) as deviceId`, | ||
`FROM '$aws/things/+/shadow/update'`, | ||
].join(' '), | ||
actions: [ | ||
{ | ||
lambda: { | ||
functionArn: fn.functionArn, | ||
}, | ||
}, | ||
], | ||
errorAction: { | ||
republish: { | ||
roleArn: ruleRole.roleArn, | ||
topic: 'errors', | ||
}, | ||
}, | ||
}, | ||
}) | ||
|
||
fn.addPermission('invokeByRule', { | ||
principal: new IAM.ServicePrincipal( | ||
'iot.amazonaws.com', | ||
) as IAM.IPrincipal, | ||
sourceArn: rule.attrArn, | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { | ||
AttributeValue, | ||
BatchWriteItemCommand, | ||
DynamoDBClient, | ||
} from '@aws-sdk/client-dynamodb' | ||
import { | ||
IoTDataPlaneClient, | ||
UpdateThingShadowCommand, | ||
} from '@aws-sdk/client-iot-data-plane' | ||
import { fromEnv } from '@nordicsemiconductor/from-env' | ||
import { transformShadowUpdateToLwM2M } from '../lwm2m/transformShadowUpdateToLwM2M.js' | ||
import { models, type LwM2MObject } from '@hello.nrfcloud.com/proto-lwm2m' | ||
import { ulid } from 'ulid' | ||
import { updatesToShadow } from '../lwm2m/objectsToShadow.js' | ||
|
||
const { tableName } = fromEnv({ | ||
tableName: 'TABLE_NAME', | ||
})(process.env) | ||
|
||
const db = new DynamoDBClient({}) | ||
const iotData = new IoTDataPlaneClient({}) | ||
const transformUpdate = transformShadowUpdateToLwM2M( | ||
models['asset_tracker_v2+AWS'].transforms, | ||
) | ||
|
||
const persist = async ( | ||
deviceId: string, | ||
objects: LwM2MObject[], | ||
): Promise<void> => { | ||
await db.send( | ||
new BatchWriteItemCommand({ | ||
RequestItems: { | ||
[tableName]: objects.map((object) => ({ | ||
PutRequest: { | ||
Item: { | ||
id: { S: ulid() }, | ||
deviceId: { S: deviceId }, | ||
ObjectId: { N: object.ObjectID.toString() }, | ||
ObjectVersion: | ||
object.ObjectVersion !== undefined | ||
? { S: object.ObjectVersion } | ||
: { S: '1.0' }, | ||
Resources: { | ||
M: Object.entries(object.Resources).reduce( | ||
(map, [k, v]) => ({ | ||
...map, | ||
[k]: convertValue(v), | ||
}), | ||
{}, | ||
), | ||
}, | ||
ttl: { | ||
N: Math.round(Date.now() / 1000 + 30 * 24 * 60 * 60).toString(), | ||
}, | ||
}, | ||
}, | ||
})), | ||
}, | ||
}), | ||
) | ||
} | ||
|
||
const convertValue = (v: string | number | boolean | Date): AttributeValue => { | ||
if (typeof v === 'number') return { N: v.toString() } | ||
if (typeof v === 'boolean') return { BOOL: v } | ||
if (typeof v === 'object' && v instanceof Date) return { S: v.toISOString() } | ||
return { S: v } | ||
} | ||
|
||
const updateShadow = async ( | ||
deviceId: string, | ||
objects: LwM2MObject[], | ||
): Promise<void> => { | ||
await iotData.send( | ||
new UpdateThingShadowCommand({ | ||
thingName: deviceId, | ||
shadowName: 'lwm2m', | ||
payload: JSON.stringify({ | ||
state: { | ||
reported: updatesToShadow(objects), | ||
}, | ||
}), | ||
}), | ||
) | ||
} | ||
|
||
/** | ||
* Store shadow updates in asset_tracker_v2 shadow format as LwM2M objects in a named shadow. | ||
* | ||
* Also store the updates in a table for historical data. | ||
*/ | ||
export const handler = async (event: { | ||
deviceId: string | ||
update: { | ||
state: { | ||
reported?: Record<string, unknown> | ||
desired?: Record<string, unknown> | ||
} | ||
} | ||
}): Promise<void> => { | ||
console.debug(JSON.stringify({ event })) | ||
const { deviceId, update } = event | ||
const objects = await transformUpdate(update) | ||
console.log( | ||
JSON.stringify({ | ||
deviceId, | ||
objects, | ||
}), | ||
) | ||
|
||
void persist(deviceId, objects) | ||
void updateShadow(deviceId, objects) | ||
} |
Oops, something went wrong.