Skip to content

Commit

Permalink
feat(wirepas): implement payload parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
coderbyheart committed Feb 7, 2024
1 parent 2c65c13 commit 81d09ac
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 59 deletions.
25 changes: 22 additions & 3 deletions wirepas-5g-mesh-gateway/ScannableArray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,29 @@ export class ScannableArray {
this.array = array
}

/**
* Returns the current character and advances the pointer.
*/
getChar(): number {
const next = this.array.at(this.index++)
if (next === undefined) throw new Error(`Out of bounds!`)
return next
const current = this.peek()
this.next()
return current
}

/**
* Returns the current character without advancing the pointer.
*/
peek(): number {
const current = this.array.at(this.index)
if (current === undefined) throw new Error(`Out of bounds!`)
return current
}

/**
* Advance the counter
*/
next(): void {
this.index++
}

hasNext(): boolean {
Expand Down
59 changes: 21 additions & 38 deletions wirepas-5g-mesh-gateway/decodePayload.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,7 @@ 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).
*/
void it('should decode a regular payload', () => {
const payload = Buffer.from(
[
// [0x01: COUNTER] [0x04] [size_t counter]
Expand Down Expand Up @@ -74,22 +54,25 @@ void describe('decodePayload()', () => {

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,
},
])
assert.deepEqual(decoded, {
counter: 49730,
timestamp: 251355997789000,
// 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,
})
})

void it('should decode a button press', () =>
assert.deepEqual(decodePayload(Buffer.from('010002', 'hex')), {
button: 2,
}))

void it('should decode a LED state change', () =>
assert.deepEqual(decodePayload(Buffer.from('030101', 'hex')), {
led: { b: true },
}))
})
108 changes: 90 additions & 18 deletions wirepas-5g-mesh-gateway/decodePayload.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { ScannableArray } from './ScannableArray.js'
export type Wirepas5GMeshNodePayload =
| { counter: number }
| { timestamp: number }
| { temperature: number }
| { button: number }
| { humidity: number }
| { raw_pressure: number }
| { raw_gas: number }
export type Wirepas5GMeshNodePayload = {
counter?: number
// Uptime in nanoseconds
timestamp?: number // e.g. 251355997789000 / 1000 / 1000 / 1000 / 60 / 60 / 24 = 2.909 days
temperature?: number
button?: number
humidity?: number
raw_pressure?: number
raw_gas?: number
led?: {
r?: boolean
g?: boolean
b?: boolean
}
}

enum MessageType {
COUNTER = 0x01, // [0x04] [size_t counter]
Expand All @@ -31,12 +38,78 @@ enum MessageType {
GAS_RAW = 0x14, // [0x04] [float raw_gas]
}

enum LED_COLOR {
RED = 0,
BLUE = 1,
GREEN = 2,
}

/*
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)).
## Button special case
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).
## LED special case
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.
*/
export const decodePayload = (
payload: Uint8Array,
): Wirepas5GMeshNodePayload[] => {
const messages: Wirepas5GMeshNodePayload[] = []
): Wirepas5GMeshNodePayload => {
const msg = new ScannableArray(payload)

let message: Wirepas5GMeshNodePayload = {}

// Button special case
if (payload.length === 3 && msg.peek() === 1) {
msg.next() // skip type
msg.next() // skip len
return { button: msg.peek() }
}

// LED special case
if (payload.length === 3 && msg.peek() === 3) {
msg.next() // skip type
const color = msg.getChar()
const state = msg.getChar()
switch (color) {
case LED_COLOR.BLUE:
return {
led: {
b: state === 1 ? true : false,
},
}
case LED_COLOR.GREEN:
return {
led: {
g: state === 1 ? true : false,
},
}
default:
return {
led: {
r: state === 1 ? true : false,
},
}
}
}

// Regular message
while (msg.hasNext()) {
const type = msg.getChar()
const len = msg.getChar()
Expand All @@ -46,11 +119,10 @@ export const decodePayload = (
switch (type) {
// Periodic message with a counter value
case MessageType.COUNTER:
messages.push({ counter: readUint(msg, len) })
message = { ...message, counter: readUint(msg, len) }
continue
case MessageType.TIMESTAMP:
// messages.push({ timestamp: readUint(msg, len) })
skip()
message = { ...message, timestamp: readUint(msg, len) }
continue
// Skip
case MessageType.IAQ:
Expand All @@ -70,16 +142,16 @@ export const decodePayload = (
skip()
continue
case MessageType.TEMPERATURE:
messages.push({ temperature: readFloat(msg, len) })
message = { ...message, temperature: readFloat(msg, len) }
continue
case MessageType.HUMIDITY:
messages.push({ humidity: readFloat(msg, len) })
message = { ...message, humidity: readFloat(msg, len) }
continue
case MessageType.PRESS_RAW:
messages.push({ raw_pressure: readFloat(msg, len) })
message = { ...message, raw_pressure: readFloat(msg, len) }
continue
case MessageType.GAS_RAW:
messages.push({ raw_gas: readFloat(msg, len) })
message = { ...message, raw_gas: readFloat(msg, len) }
continue
default:
console.error(`Unknown message type`, type)
Expand All @@ -88,7 +160,7 @@ export const decodePayload = (
}
}

return messages
return message
}

const readUint = (message: ScannableArray, numBytes: number): number => {
Expand Down

0 comments on commit 81d09ac

Please sign in to comment.