Skip to content

Commit

Permalink
feat(memfault): fetch reboots for devices on button press
Browse files Browse the repository at this point in the history
If a device publishes a button press for the button 42
the backend will poll every 15 seconds for up to two
minutes for reboots on the Memfault API.
If a reboot is found that is newer than the button press
it is sent to the frontend.
  • Loading branch information
coderbyheart committed Mar 21, 2024
1 parent 40178f6 commit 1847a11
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 79 deletions.
1 change: 1 addition & 0 deletions cdk/BackendLambdas.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ type BackendLambdas = {
updatesToLwM2M: PackedLambda
publishLwM2MShadowsToJSON: PackedLambda
memfault: PackedLambda
memfaultPollForReboots: PackedLambda
}
2 changes: 2 additions & 0 deletions cdk/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const packagesInLayer: string[] = [
'jsonata',
'mqtt',
'@protobuf-ts/runtime',
'p-retry',
]
const pack = async (
id: string,
Expand Down Expand Up @@ -53,6 +54,7 @@ new BackendApp({
nrplusGatewayScan: await pack('nrplusGatewayScan'),
updatesToLwM2M: await pack('updatesToLwM2M'),
memfault: await pack('memfault'),
memfaultPollForReboots: await pack('memfaultPollForReboots'),
// For hello.nrfcloud.com/map
publishLwM2MShadowsToJSON: await pack('publishLwM2MShadowsToJSON'),
},
Expand Down
98 changes: 96 additions & 2 deletions cdk/resources/Memfault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
aws_s3 as S3,
aws_iam as IAM,
aws_lambda as Lambda,
aws_iot as IoT,
Stack,
RemovalPolicy,
} from 'aws-cdk-lib'
Expand All @@ -29,6 +30,7 @@ export class Memfault extends Construct {
}: {
lambdaSources: {
memfault: PackedLambda
memfaultPollForReboots: PackedLambda
}
baseLayer: Lambda.ILayerVersion
assetTrackerStackName: string
Expand Down Expand Up @@ -72,8 +74,7 @@ export class Memfault extends Construct {
ASSET_TRACKER_STACK_NAME: assetTrackerStackName,
NODE_NO_WARNINGS: '1',
BUCKET: this.bucket.bucketName,
WEBSOCKET_CONNECTIONS_TABLE_NAME:
websocketAPI.connectionsTable.tableName,
CONNECTIONS_TABLE_NAME: websocketAPI.connectionsTable.tableName,
},
initialPolicy: [
new IAM.PolicyStatement({
Expand Down Expand Up @@ -104,5 +105,98 @@ export class Memfault extends Construct {
principal: new IAM.ServicePrincipal('events.amazonaws.com') as IPrincipal,
sourceArn: rule.ruleArn,
})

// When a devices publishes button press 42, poll the Memfault API for an update
const pollForRebootsFn = new Lambda.Function(this, 'pollForRebootsFn', {
handler: lambdaSources.memfaultPollForReboots.handler,
architecture: Lambda.Architecture.ARM_64,
runtime: Lambda.Runtime.NODEJS_20_X,
timeout: Duration.seconds(120),
memorySize: 1792,
code: Lambda.Code.fromAsset(
lambdaSources.memfaultPollForReboots.lambdaZipFile,
),
description:
'Poll the Memfault API for an update after a device publishes a button event for button 42',
layers: [baseLayer],
environment: {
VERSION: this.node.tryGetContext('version'),

Check warning on line 123 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,
CONNECTIONS_TABLE_NAME: websocketAPI.connectionsTable.tableName,
WEBSOCKET_MANAGEMENT_API_URL: websocketAPI.websocketManagementAPIURL,
},
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: ['execute-api:ManageConnections'],
resources: [websocketAPI.websocketAPIArn],
}),
new IAM.PolicyStatement({
actions: ['iot:DescribeThing'],
resources: ['*'],
}),
],
...new LambdaLogGroup(this, 'pollForRebootsFnLogs'),
})

websocketAPI.connectionsTable.grantFullAccess(pollForRebootsFn)

const button42RuleRole = new IAM.Role(this, 'button42RuleRole', {
assumedBy: new IAM.ServicePrincipal('iot.amazonaws.com'),
inlinePolicies: {
rootPermissions: new IAM.PolicyDocument({
statements: [
new IAM.PolicyStatement({
actions: ['iot:Publish'],
resources: [
`arn:aws:iot:${Stack.of(this).region}:${Stack.of(this).account}:topic/errors`,
],
}),
],
}),
},
})

const button42Rule = new IoT.CfnTopicRule(this, 'button42Rule', {
topicRulePayload: {
awsIotSqlVersion: '2016-03-23',
description:
'Trigger a fetch of the Memfault data when a device publishes a button event for button 42',
ruleDisabled: false,
sql: [
'SELECT topic(1) as deviceId,',
'btn.ts as ts,',
`parse_time("yyyy-MM-dd'T'HH:mm:ss.S'Z'", timestamp()) as timestamp`,
"FROM '+/messages'",
'WHERE btn.v = 42',
].join(' '),
actions: [
{
lambda: {
functionArn: pollForRebootsFn.functionArn,
},
},
],
errorAction: {
republish: {
roleArn: button42RuleRole.roleArn,
topic: 'errors',
},
},
},
})

pollForRebootsFn.addPermission('storeMessagesRule', {
principal: new IAM.ServicePrincipal('iot.amazonaws.com'),
sourceArn: button42Rule.attrArn,
})
}
}
90 changes: 15 additions & 75 deletions lambda/memfault.ts
Original file line number Diff line number Diff line change
@@ -1,88 +1,28 @@
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { IoTClient } from '@aws-sdk/client-iot'
import { GetParametersByPathCommand, SSMClient } from '@aws-sdk/client-ssm'
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { SSMClient } from '@aws-sdk/client-ssm'
import { fromEnv } from '@nordicsemiconductor/from-env'
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { getActiveConnections } from './notifyClients.js'
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { listThingsInGroup } from './listThingsInGroup.js'
import { createAPIClient, type Reboot } from './memfault/api.js'
import { getActiveConnections } from './notifyClients.js'

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

const {
stackName,
nrfAssetTrackerStackName,
bucket,
websocketConnectionsTableName,
} = fromEnv({
stackName: 'STACK_NAME',
nrfAssetTrackerStackName: 'ASSET_TRACKER_STACK_NAME',
bucket: 'BUCKET',
websocketConnectionsTableName: 'WEBSOCKET_CONNECTIONS_TABLE_NAME',
})(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!`)

const getActive = getActiveConnections(db, websocketConnectionsTableName)
const { stackName, nrfAssetTrackerStackName, bucket, connectionsTableName } =
fromEnv({
stackName: 'STACK_NAME',
nrfAssetTrackerStackName: 'ASSET_TRACKER_STACK_NAME',
bucket: 'BUCKET',
connectionsTableName: 'CONNECTIONS_TABLE_NAME',
})(process.env)

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 getActive = getActiveConnections(db, connectionsTableName)

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
},
}
const api = await createAPIClient(ssm, stackName)

const listThings = listThingsInGroup(iot)

Expand Down
68 changes: 68 additions & 0 deletions lambda/memfault/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { GetParametersByPathCommand, SSMClient } from '@aws-sdk/client-ssm'

export const createAPIClient = async (
ssm: SSMClient,
stackName: string,
): Promise<{
getLastReboots: (deviceId: string) => Promise<null | Array<Reboot>>
}> => {
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!`)

return {
getLastReboots: async (deviceId) => {
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
},
}
}

export 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
}
}
Loading

0 comments on commit 1847a11

Please sign in to comment.