Skip to content

Commit

Permalink
feat(wirepas): use new payload format
Browse files Browse the repository at this point in the history
  • Loading branch information
coderbyheart committed Feb 5, 2024
1 parent 03b7bb4 commit 8471ff3
Show file tree
Hide file tree
Showing 12 changed files with 349 additions and 120 deletions.
13 changes: 2 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,10 @@ aws ssm put-parameter --name thingy-rocks-backend-Wirepas5GMeshGatewayEndpoint -
npx cdk deploy
```

## Support for managing Wirepas 5G Mesh nodes

For interacting with these nodes,

1. Create a thing type `mesh-node` (they cannot be created using
CloudFormation).
1. Assign the thing type `mesh-node` to the thing which should act as a 5G Mesh
Node.

The thing type is check when an state update is received from the UI.

### Running the Wirepas 5G Mesh Gateway

Create a thing type `wirepas-5g-mesh-gateway`.

Configure the gateway settings using the `.envrc` (see
[the example](./envrc.example)).

Expand Down
16 changes: 6 additions & 10 deletions cdk/resources/Wirepas5GMeshGateway.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
import type { Stack } from 'aws-cdk-lib'
import { aws_iam as IAM } from 'aws-cdk-lib'
import { Construct } from 'constructs'
import type { WebsocketAPI } from './WebsocketAPI'

export class Wirepas5GMeshGateway extends Construct {
public readonly accessKey
constructor(parent: Stack, { websocketAPI }: { websocketAPI: WebsocketAPI }) {
constructor(parent: Stack) {
super(parent, 'Wirepas5GMeshGateway')

const gatewayUser = new IAM.User(this, 'gatewayUser')
gatewayUser.addToPolicy(
new IAM.PolicyStatement({
actions: ['iot:DescribeEndpoint'],
actions: [
'iot:DescribeEndpoint',
'iot:UpdateThingShadow',
'iot:ListThings',
],
resources: [`*`],
}),
)
gatewayUser.addToPolicy(
new IAM.PolicyStatement({
actions: ['execute-api:ManageConnections'],
resources: [websocketAPI.websocketAPIArn],
}),
)
websocketAPI.connectionsTable.grantFullAccess(gatewayUser)

this.accessKey = new IAM.CfnAccessKey(this, 'accessKey', {
userName: gatewayUser.userName,
Expand Down
2 changes: 1 addition & 1 deletion cdk/stacks/BackendStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export class BackendStack extends Stack {
baseLayer,
})

const wirepasGateway = new Wirepas5GMeshGateway(this, { websocketAPI: api })
const wirepasGateway = new Wirepas5GMeshGateway(this)

// Outputs
new CfnOutput(this, 'WebSocketURI', {
Expand Down
2 changes: 2 additions & 0 deletions lambda/notifyClients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export type DeviceEvent = {
deviceAlias?: string
// The fixed geo-location of the device,
deviceLocation?: string // e.g.: 63.42115901688979,10.437200141182338
// The thingy type
deviceType?: string
} & (
| {
reported: Record<string, any>
Expand Down
21 changes: 14 additions & 7 deletions lambda/withDeviceAlias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,26 @@ export const withDeviceAlias = <N extends ReturnType<typeof notifyClients>>(
return (notifier: N) =>
async (event: Parameters<N>[0]): Promise<void> => {
if (!('deviceId' in event)) return notifier(event)
const { alias: deviceAlias, location } = await info(event.deviceId)
if (deviceAlias === undefined) return notifier(event)
const { alias: deviceAlias, location, type } = await info(event.deviceId)
return notifier({
...event,
deviceAlias,
deviceLocation: location,
deviceType: type,
})
}
}

const deviceInfo: Record<string, { alias?: string; location?: string }> = {}
const deviceInfo: Record<
string,
{ alias?: string; location?: string; type?: string }
> = {}

export const getDeviceInfo =
(iot: IoTClient) =>
async (deviceId: string): Promise<{ alias?: string; location?: string }> => {
async (
deviceId: string,
): Promise<{ alias?: string; location?: string; type?: string }> => {
const info =
deviceInfo[deviceId] ?? (await getDeviceAttributes(iot)(deviceId))
if (!(deviceId in deviceInfo)) deviceInfo[deviceId] = info
Expand All @@ -36,11 +41,13 @@ export const getDeviceInfo =
}

const getDeviceAttributes = (iot: IoTClient) => async (deviceId: string) => {
const { name, location } =
(await iot.send(new DescribeThingCommand({ thingName: deviceId })))
?.attributes ?? {}
const { attributes, thingTypeName } = await iot.send(
new DescribeThingCommand({ thingName: deviceId }),
)
const { name, location } = attributes ?? {}
return {
alias: name,
location,
type: thingTypeName,
}
}
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@swc/core": "1.4.0",
"@types/aws-lambda": "8.10.133",
"@types/glob": "8.1.0",
"@types/lodash-es": "4.17.12",
"@types/node": "20.11.16",
"@types/yazl": "2.4.5",
"@typescript-eslint/eslint-plugin": "6.20.0",
Expand Down Expand Up @@ -95,6 +96,7 @@
"@sinclair/typebox": "0.32.13",
"ajv": "8.12.0",
"jsonata": "2.0.3",
"lodash-es": "4.17.21",
"mqtt": "5.3.5",
"protobufjs": "7.2.6"
}
Expand Down
10 changes: 3 additions & 7 deletions wirepas-5g-mesh-gateway/ScannableArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,13 @@ export class ScannableArray {
this.array = array
}

getChar(): number | undefined {
return this.array.at(this.index)
}

getCharNext(): number {
getChar(): number {
const next = this.array.at(this.index++)
if (next === undefined) throw new Error(`Out of bounds!`)
return next
}

next(): void {
this.index++
hasNext(): boolean {
return this.array.at(this.index + 1) !== undefined
}
}
95 changes: 95 additions & 0 deletions wirepas-5g-mesh-gateway/decodePayload.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, it } from 'node:test'
import { decodePayload } from './decodePayload.js'
import assert from 'node:assert/strict'

void describe('decodePayload()', () => {
void it('should decode the payload', () => {
/*
For this payload (92 bytes), it is on TLV format: you have the information ID (1 byte), the data length (1 byte) and the data (n bytes).
For example, data 01 corresponds to the counter (4 bytes long, data is 42 c2 00 00 or 49730).
The relevant data starts with 0F for the temperature (here, it is this part: 0f 04 0a d7 c3 41, which gives a temperature of 24.48°C (it is a float32)).
Also, we send data starting with 01 but with a different length (3 bytes) which corresponds to a key press.
Example: 01 00 02 (because there is only one button).
You may see payloads starting with 03 (3 bytes): it is when LED status/color changes.
In this case, color is the following:
Byte 1: ID (0x03)
Byte 2: Color. 0x00: red, 0x01: blue, 0x02: green
Byte 3: State. 0x00: off, 0x01: on.
We send this payload when requested (response to get LED status (starts with 0x82)) or when setting LED (message 0x81), as an acknowledgement (to confirm color/status has changed).
*/
const payload = Buffer.from(
[
// [0x01: COUNTER] [0x04] [size_t counter]
'01 04 42 c2 00 00',
// [0x02: TIMESTAMP] [0x08] [int64_t timestamp]
'02 08 48 db f5 60 9b e4 00 00',
// [0x03: IAQ] [0x02] [uint16_t iaq]
'03 02 00 00',
// [0x04: IAQ_ACC] [0x01] [uint8_t iaq_acc]
'04 01 00',
// [0x05: SIAQ] [0x02] [uint16_t siaq]
'05 02 00 00',
// [0x06: SIAQ_ACC] [0x01] [uint8_t siaq_acc]
'06 01 00',
// [0x07: SENSOR_STATUS] [0x01] [uint8_t sensor_status]
'07 01 00',
// [0x08: SENSOR_STABILITY][0x01] [uint8_t sensor_stable]
'08 01 00',
// [0x09: GAS] [0x01] [uint8_t gas]
'09 01 00',
// [0x0A: GAS_ACC] [0x01] [uint8_t gas_acc]
'0a 01 00',
// [0x0B: VOC] [0x02] [uint16_t voc]
'0b 02 00 00',
// [0x0C: VOC_ACC] [0x01] [uint8_t voc_acc]
'0c 01 00',
// [0x0D: CO2] [0x02] [uint16_t co2]
'0d 02 00 00',
// [0x0E: CO2_ACC] [0x01] [uint8_t co2_acc]
'0e 01 00',
// [0x0F: TEMPERATURE] [0x04] [float temperature]
'0f 04 0a d7 c3 41',
// [0x10: HUMIDITY] [0x04] [float humidity]
'10 04 68 91 8d 41',
// [0x12: HUM_RAW] [0x04] [float raw_humidity]
'12 04 00 40 8a 46',
// [0x11: TEMP_RAW] [0x04] [float raw_temperature]
'11 04 00 00 19 45',
// [0x13: PRESS_RAW] [0x04] [float raw_pressure]
'13 04 80 f2 c3 47',
// [0x14: GAS_RAW] [0x04] [float raw_gas]
'14 04 00 ca 9e 47',
]
.join('')
.replaceAll(' ', ''),
'hex',
)

const decoded = decodePayload(payload)

assert.deepEqual(decoded, [
{ counter: 49730 },
{
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
temperature: 24.479999542236328,
},
{
// eslint-disable-next-line @typescript-eslint/no-loss-of-precision
humidity: 17.695999145507812,
},
{
raw_pressure: 100325,
},
{
raw_gas: 81300,
},
])
})
})
Loading

0 comments on commit 8471ff3

Please sign in to comment.