Skip to content

Commit

Permalink
feat(memfault): fetch reboots and publish
Browse files Browse the repository at this point in the history
  • Loading branch information
coderbyheart committed Mar 14, 2024
1 parent 46615f0 commit 3e5f3fd
Show file tree
Hide file tree
Showing 9 changed files with 1,354 additions and 1,127 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,13 @@ Run as a service using systemd:
```bash
systemd-run -E GATEWAY_MQTT_ENDPOINT=${GATEWAY_MQTT_ENDPOINT} -E GATEWAY_AWS_ACCESS_KEY_ID=${GATEWAY_AWS_ACCESS_KEY_ID} -E GATEWAY_REGION=${GATEWAY_REGION} -E GATEWAY_AWS_SECRET_ACCESS_KEY=${GATEWAY_AWS_SECRET_ACCESS_KEY} --working-directory ${PWD} npx tsx wirepas-5g-mesh-gateway/gateway.ts
```

### Memfault integration

Configure these SSM parameters:

```bash
aws ssm put-parameter --name /thingy-rocks-backend/memfault/organizationAuthToken --type String --value <Memfault Organization Auth Token>
aws ssm put-parameter --name /thingy-rocks-backend/memfault/organizationId --type String --value <Memfault Organization ID>
aws ssm put-parameter --name /thingy-rocks-backend/memfault/projectId --type String --value <Memfault Project ID>
```
1 change: 1 addition & 0 deletions cdk/BackendLambdas.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ type BackendLambdas = {
nrplusGatewayScan: PackedLambda
updatesToLwM2M: PackedLambda
publishLwM2MShadowsToJSON: PackedLambda
memfault: PackedLambda
}
2 changes: 2 additions & 0 deletions cdk/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ new BackendApp({
parseSinkMessages: await pack('parseSinkMessages'),
nrplusGatewayScan: await pack('nrplusGatewayScan'),
updatesToLwM2M: await pack('updatesToLwM2M'),
memfault: await pack('memfault'),
// For hello.nrfcloud.com/map
publishLwM2MShadowsToJSON: await pack('publishLwM2MShadowsToJSON'),
},
layer: await packLayer({
Expand Down
102 changes: 102 additions & 0 deletions cdk/resources/Memfault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
Duration,
aws_events as Events,
aws_events_targets as EventsTargets,
aws_s3 as S3,
aws_iam as IAM,
aws_lambda as Lambda,
Stack,
RemovalPolicy,
} from 'aws-cdk-lib'
import type { IPrincipal } from 'aws-cdk-lib/aws-iam/index.js'
import { Construct } from 'constructs'
import type { PackedLambda } from '../backend.js'
import { LambdaLogGroup } from './LambdaLogGroup.js'

/**
* Pull Memfault data for devices
*/
export class Memfault extends Construct {
public readonly bucket: S3.Bucket
public constructor(
parent: Construct,
{
lambdaSources,
baseLayer,
assetTrackerStackName,
}: {
lambdaSources: {
memfault: PackedLambda
}
baseLayer: Lambda.ILayerVersion
assetTrackerStackName: string
},
) {
super(parent, 'Memfault')

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://world.thingy.rocks', 'http://localhost:*'],
allowedMethods: [S3.HttpMethods.GET],
},
],
})

const fn = new Lambda.Function(this, 'fn', {
handler: lambdaSources.memfault.handler,
architecture: Lambda.Architecture.ARM_64,
runtime: Lambda.Runtime.NODEJS_20_X,
timeout: Duration.seconds(60),
memorySize: 1792,
code: Lambda.Code.fromAsset(lambdaSources.memfault.lambdaZipFile),
description: 'Pull Memfault data for devices and put them in the shadow',
layers: [baseLayer],
environment: {
VERSION: this.node.tryGetContext('version'),

Check warning on line 67 in cdk/resources/Memfault.ts

View workflow job for this annotation

GitHub Actions / tests

Unsafe assignment of an `any` value
STACK_NAME: Stack.of(this).stackName,
ASSET_TRACKER_STACK_NAME: assetTrackerStackName,
NODE_NO_WARNINGS: '1',
BUCKET: this.bucket.bucketName,
},
initialPolicy: [
new IAM.PolicyStatement({
actions: ['ssm:GetParametersByPath'],
resources: [
`arn:aws:ssm:${Stack.of(this).region}:${Stack.of(this).account}:parameter/${Stack.of(this).stackName}/memfault/*`,
],
}),
new IAM.PolicyStatement({
actions: ['iot:ListThingsInThingGroup'],
resources: ['*'],
}),
],
...new LambdaLogGroup(this, 'fnLogs'),
})

this.bucket.grantWrite(fn)

const rule = new Events.Rule(this, 'Rule', {
schedule: Events.Schedule.expression('rate(5 minutes)'),
description: `Invoke the Memfault lambda`,
enabled: true,
targets: [new EventsTargets.LambdaFunction(fn)],
})

fn.addPermission('InvokeByEvents', {
principal: new IAM.ServicePrincipal('events.amazonaws.com') as IPrincipal,
sourceArn: rule.ruleArn,
})
}
}
12 changes: 12 additions & 0 deletions cdk/stacks/BackendStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ 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'
import { Memfault } from '../resources/Memfault.js'

export class BackendStack extends Stack {
public constructor(
Expand Down Expand Up @@ -112,6 +113,12 @@ export class BackendStack extends Stack {
baseLayer,
})

const memfault = new Memfault(this, {
assetTrackerStackName,
baseLayer,
lambdaSources,
})

// Outputs
new CfnOutput(this, 'WebSocketURI', {
exportName: `${this.stackName}:WebSocketURI`,
Expand Down Expand Up @@ -153,6 +160,11 @@ export class BackendStack extends Stack {
value: `https://${lwm2mPublicShadows.bucket.bucketDomainName}/`,
exportName: `${this.stackName}:publicLwM2MShadowsBucketURL`,
})

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

Expand Down
112 changes: 112 additions & 0 deletions lambda/memfault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { IoTClient, ListThingsInThingGroupCommand } from '@aws-sdk/client-iot'
import { GetParametersByPathCommand, SSMClient } from '@aws-sdk/client-ssm'
import { fromEnv } from '@nordicsemiconductor/from-env'
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'

const ssm = new SSMClient({})
const iot = new IoTClient({})
const s3 = new S3Client({})

const { stackName, nrfAssetTrackerStackName, bucket } = fromEnv({
stackName: 'STACK_NAME',
nrfAssetTrackerStackName: 'ASSET_TRACKER_STACK_NAME',
bucket: 'BUCKET',
})(process.env)

const Prefix = `/${stackName}/memfault/`
const { organizationAuthToken, organizationId, projectId } = (
(
await ssm.send(
new GetParametersByPathCommand({
Path: Prefix,
}),
)
)?.Parameters ?? []
).reduce(
(params, p) => ({
...params,
[(p.Name ?? '').replace(Prefix, '')]: p.Value ?? '',
}),
{} as Record<string, string>,
)

if (
organizationAuthToken === undefined ||
organizationId === undefined ||
projectId === undefined
)
throw new Error(`Memfault settings not configured!`)

type Reboot = {
type: 'memfault'
mcu_reason_register: null
time: string // e.g. '2024-03-14T07:26:37.270000+00:00'
reason: number // e.g. 7
software_version: {
version: string // e.g. '1.11.1+thingy91.low-power.memfault'
id: number // e.g.504765
software_type: {
id: number //e.g. 32069;
name: string // e.g. 'thingy_world'
}
archived: boolean
}
}

const api = {
getLastReboots: async (deviceId: string): Promise<null | Array<Reboot>> => {
const res = await fetch(
`https://api.memfault.com/api/v0/organizations/${organizationId}/projects/${projectId}/devices/${deviceId}/reboots?${new URLSearchParams(
{
since: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
},
).toString()}`,
{
headers: new Headers({
Authorization: `Basic ${Buffer.from(`:${organizationAuthToken}`).toString('base64')}`,
}),
},
)
if (!res.ok) return null
return (await res.json()).data
},
}

/**
* Pull data from Memfault about all devices
*/
export const handler = async (): Promise<void> => {
const { things } = await iot.send(
new ListThingsInThingGroupCommand({
thingGroupName: nrfAssetTrackerStackName,
}),
)
const deviceReboots: Record<string, Array<Reboot>> = {}
for (const thing of things ?? []) {
const reboots = await api.getLastReboots(thing)
if (reboots === null) {
console.debug(thing, `No data found.`)
continue
}
if (reboots.length === 0) {
console.debug(thing, `No reboots in the last 24 hours.`)
continue
}
deviceReboots[thing] = reboots
console.debug(thing, `Updated`)
}

await s3.send(
new PutObjectCommand({
Bucket: bucket,
Key: 'device-reboots.json',
ContentType: 'application/json',
CacheControl: 'public, max-age=300',
Body: JSON.stringify({
'@context':
'https://github.com/NordicPlayground/thingy-rocks-cloud-aws-js/Memfault/reboots',
reboots: deviceReboots,
}),
}),
)
}
4 changes: 2 additions & 2 deletions lwm2m/transformShadowUpdateToLwM2M.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import jsonata from 'jsonata'
import {
senMLtoLwM2M,
type LwM2MObjectInstance,
type Transformer,
type Transform,
definitions,
} from '@hello.nrfcloud.com/proto-lwm2m'

Expand All @@ -17,7 +17,7 @@ type Update = {
* Very simple implementation of a converter.
*/
export const transformShadowUpdateToLwM2M = (
transformers: Readonly<Array<Transformer>>,
transformers: Readonly<Array<Transform>>,
): ((update: Update) => Promise<ReturnType<typeof senMLtoLwM2M>>) => {
// Turn the JSONata in the transformers into executable functions
const transformerFns: Array<{
Expand Down
Loading

0 comments on commit 3e5f3fd

Please sign in to comment.