Skip to content

Commit

Permalink
feat: publish LwM2M shadows for hello.nrfcloud.com/map
Browse files Browse the repository at this point in the history
  • Loading branch information
coderbyheart committed Feb 23, 2024
1 parent 9100231 commit d4fbe5c
Show file tree
Hide file tree
Showing 10 changed files with 709 additions and 65 deletions.
1 change: 1 addition & 0 deletions cdk/BackendLambdas.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ type BackendLambdas = {
parseSinkMessages: PackedLambda
nrplusGatewayScan: PackedLambda
updatesToLwM2M: PackedLambda
publishLwM2MShadowsToJSON: PackedLambda
}
1 change: 1 addition & 0 deletions cdk/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ new BackendApp({
parseSinkMessages: await pack('parseSinkMessages'),
nrplusGatewayScan: await pack('nrplusGatewayScan'),
updatesToLwM2M: await pack('updatesToLwM2M'),
publishLwM2MShadowsToJSON: await pack('publishLwM2MShadowsToJSON'),
},
layer: await packLayer({
id: 'baseLayer',
Expand Down
18 changes: 18 additions & 0 deletions cdk/resources/LambdaLogGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Construct } from 'constructs'
import { aws_logs as Logs, Names, Stack } from 'aws-cdk-lib'

export class LambdaLogGroup extends Construct {
public readonly logGroup: Logs.LogGroup
constructor(
parent: Construct,
id: string,
retention = Logs.RetentionDays.ONE_DAY,
) {
super(parent, id)
this.logGroup = new Logs.LogGroup(this, 'logGroup', {
retention,
logGroupName: `/${Stack.of(this).stackName}/fn/${id}-${Names.uniqueId(this)}`,
logGroupClass: Logs.LogGroupClass.STANDARD, // INFREQUENT_ACCESS does not support custom metrics
})
}
}
87 changes: 87 additions & 0 deletions cdk/resources/hello.nrfcloud.com/PublicLwM2MShadows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
Duration,
aws_iam as IAM,
aws_lambda as Lambda,
RemovalPolicy,
aws_s3 as S3,
aws_events_targets as EventTargets,
aws_events as Events,
} from 'aws-cdk-lib'
import { Construct } from 'constructs'
import type { PackedLambda } from '../../backend.js'
import { LambdaLogGroup } from '../LambdaLogGroup.js'

/**
* Publish a JSON of all LwM2M shadows so https://hello.nrfcloud.com/map can show them.
*/
export class PublicLwM2MShadows extends Construct {
public readonly bucket: S3.Bucket
constructor(
parent: Construct,
{
baseLayer,
lambdaSources,
}: {
baseLayer: Lambda.ILayerVersion
lambdaSources: {
publishLwM2MShadowsToJSON: PackedLambda
}
},
) {
super(parent, 'PublicLwM2MShadows')

this.bucket = new S3.Bucket(this, 'bucket', {
autoDeleteObjects: true,
removalPolicy: RemovalPolicy.DESTROY,
publicReadAccess: true,
websiteIndexDocument: 'index.html',
blockPublicAccess: {
blockPublicAcls: false,
ignorePublicAcls: false,
restrictPublicBuckets: false,
blockPublicPolicy: false,
},
objectOwnership: S3.ObjectOwnership.OBJECT_WRITER,
cors: [
{
allowedOrigins: ['https://hello.nrfcloud.com', 'http://localhost:*'],
allowedMethods: [S3.HttpMethods.GET],
},
],
})

const fn = new Lambda.Function(this, 'fn', {
handler: lambdaSources.publishLwM2MShadowsToJSON.handler,
architecture: Lambda.Architecture.ARM_64,
runtime: Lambda.Runtime.NODEJS_20_X,
timeout: Duration.minutes(1),
memorySize: 1792,
code: Lambda.Code.fromAsset(
lambdaSources.publishLwM2MShadowsToJSON.lambdaZipFile,
),
description:
'Provides the LwM2M shadow of the devices to https://hello.nrfcloud.com/map',
layers: [baseLayer],
environment: {
VERSION: this.node.tryGetContext('version'),
NODE_NO_WARNINGS: '1',
BUCKET: this.bucket.bucketName,
},
...new LambdaLogGroup(this, 'devicesFnLogs'),
initialPolicy: [
new IAM.PolicyStatement({
actions: ['iot:SearchIndex', 'iot:DescribeThing'],
resources: ['*'],
}),
],
})

this.bucket.grantWrite(fn)

const rule = new Events.Rule(this, 'rule', {
description: `Rule to schedule publishLwM2MShadowsToJSON lambda invocations`,
schedule: Events.Schedule.rate(Duration.minutes(1)),
})
rule.addTarget(new EventTargets.LambdaFunction(fn))
}
}
11 changes: 11 additions & 0 deletions cdk/stacks/BackendStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Wirepas5GMeshGateway } from '../resources/Wirepas5GMeshGateway.js'
import { STACK_NAME } from './stackName.js'
import { NRPlusGateway } from '../resources/NRPlusGateway.js'
import { LwM2M } from '../resources/LwM2M.js'
import { PublicLwM2MShadows } from '../resources/hello.nrfcloud.com/PublicLwM2MShadows.js'

export class BackendStack extends Stack {
public constructor(
Expand Down Expand Up @@ -105,6 +106,11 @@ export class BackendStack extends Stack {

const wirepasGateway = new Wirepas5GMeshGateway(this)

const lwm2mPublicShadows = new PublicLwM2MShadows(this, {
lambdaSources,
baseLayer,
})

// Outputs
new CfnOutput(this, 'WebSocketURI', {
exportName: `${this.stackName}:WebSocketURI`,
Expand Down Expand Up @@ -141,6 +147,11 @@ export class BackendStack extends Stack {
value: wirepasGateway.accessKey.attrSecretAccessKey,
exportName: `${this.stackName}:wirepasGatewayUserSecretAccessKey`,
})

new CfnOutput(this, 'publicLwM2MShadowsBucketURL', {
value: `https://${lwm2mPublicShadows.bucket.bucketDomainName}/`,
exportName: `${this.stackName}:publicLwM2MShadowsBucketURL`,
})
}
}

Expand Down
61 changes: 4 additions & 57 deletions lambda/onMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,14 @@ import {
IoTDataPlaneClient,
PublishCommand,
} from '@aws-sdk/client-iot-data-plane'
import {
DescribeThingCommand,
IoTClient,
SearchIndexCommand,
} from '@aws-sdk/client-iot'
import { DescribeThingCommand, IoTClient } from '@aws-sdk/client-iot'
import {
UpdateThingShadowCommand,
type UpdateThingShadowCommandInput,
} from '@aws-sdk/client-iot-data-plane'
import { ApiGatewayManagementApi } from '@aws-sdk/client-apigatewaymanagementapi'
import { sendEvent } from './notifyClients.js'
import type { LwM2MObjectInstance } from '@hello.nrfcloud.com/proto-lwm2m'
import { shadowToObjects } from '../lwm2m/shadowToObjects.js'
import { getDeviceInfo } from './withDeviceAlias.js'
import { fetchLwM2MShadows } from '../lwm2m/fetchLwM2MShadows.js'

const { TableName, websocketManagementAPIURL } = fromEnv({
TableName: 'CONNECTIONS_TABLE_NAME',
Expand Down Expand Up @@ -77,7 +71,7 @@ const apiGwManagementClient = new ApiGatewayManagementApi({

const send = sendEvent(apiGwManagementClient)

const deviceInfo = getDeviceInfo(iot)
const fetchLwM2M = fetchLwM2MShadows(iot)

export const handler = async (
event: APIGatewayProxyWebsocketEventV2,
Expand Down Expand Up @@ -129,54 +123,7 @@ export const handler = async (

if (message.data === 'LWM2M-shadows') {
// Publish LwM2M shadows
const { things } = await iot.send(
new SearchIndexCommand({
// Find all things which have an LwM2M shadow
queryString: 'shadow.name.lwm2m.hasDelta:*',
}),
)
const shadows = (
await Promise.all<{
deviceId: string
alias?: string
objects: LwM2MObjectInstance[]
}>(
(things ?? []).map(async ({ thingName, shadow }) => {
const alias = (await deviceInfo(thingName as string)).alias
const reported = JSON.parse(shadow ?? '{}').name.lwm2m.reported
if (reported === undefined)
return {
deviceId: thingName as string,
alias,
objects: [],
}

try {
return {
deviceId: thingName as string,
alias,
objects: shadowToObjects(reported),
}
} catch (err) {
console.error(`Failed to convert shadow for thing ${thingName}`)
console.log(
JSON.stringify({
thingName,
shadow: {
reported,
},
}),
)
console.error(err)
return {
deviceId: thingName as string,
alias,
objects: [],
}
}
}),
)
).filter(({ objects }) => objects.length > 0)
const shadows = await fetchLwM2M()

console.log(
JSON.stringify({
Expand Down
29 changes: 29 additions & 0 deletions lambda/publishLwM2MShadowsToJSON.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { IoTClient } from '@aws-sdk/client-iot'
import { fetchLwM2MShadows } from '../lwm2m/fetchLwM2MShadows.js'
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { fromEnv } from '@nordicsemiconductor/from-env'

const iot = new IoTClient({})
const fetchShadows = fetchLwM2MShadows(iot)
const s3 = new S3Client({})
const { bucket } = fromEnv({ bucket: 'BUCKET' })(process.env)

export const handler = async (): Promise<void> => {
await s3.send(
new PutObjectCommand({
Bucket: bucket,
Key: 'lwm2m-shadows.json',
ContentType: 'application/json',
CacheControl: 'public, max-age=60',
Body: JSON.stringify({
'@context': 'https://github.com/hello-nrfcloud/proto/map/devices',
devices: (await fetchShadows()).map(({ deviceId, objects }) => ({
'@context': 'https://github.com/hello-nrfcloud/proto/map/device',
id: deviceId,
model: 'world.thingy.rocks',
state: objects,
})),
}),
}),
)
}
62 changes: 62 additions & 0 deletions lwm2m/fetchLwM2MShadows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { IoTClient, SearchIndexCommand } from '@aws-sdk/client-iot'
import type { LwM2MObjectInstance } from '@hello.nrfcloud.com/proto-lwm2m'
import { shadowToObjects } from './shadowToObjects.js'
import { getDeviceInfo } from '../lambda/withDeviceAlias.js'

type LwM2MShadow = {
deviceId: string
alias?: string
objects: LwM2MObjectInstance[]
}

export const fetchLwM2MShadows = (
iot: IoTClient,
): (() => Promise<LwM2MShadow[]>) => {
const deviceInfo = getDeviceInfo(iot)
return async () => {
const { things } = await iot.send(
new SearchIndexCommand({
// Find all things which have an LwM2M shadow
queryString: 'shadow.name.lwm2m.hasDelta:*',
}),
)
return (
await Promise.all<LwM2MShadow>(
(things ?? []).map(async ({ thingName, shadow }) => {
const alias = (await deviceInfo(thingName as string)).alias
const reported = JSON.parse(shadow ?? '{}').name.lwm2m.reported
if (reported === undefined)
return {
deviceId: thingName as string,
alias,
objects: [],
}

try {
return {
deviceId: thingName as string,
alias,
objects: shadowToObjects(reported),
}
} catch (err) {
console.error(`Failed to convert shadow for thing ${thingName}`)
console.log(
JSON.stringify({
thingName,
shadow: {
reported,
},
}),
)
console.error(err)
return {
deviceId: thingName as string,
alias,
objects: [],
}
}
}),
)
).filter(({ objects }) => objects.length > 0)
}
}
Loading

0 comments on commit d4fbe5c

Please sign in to comment.