Using strong-typed codecs and interfaces

This example covers some more complicated examples using typedState and shows how IDE hinting can use this information for a better development experience.

We'll be using a MSGID enum for variable management, custom types, and the ElectricUIDeveloperState enforced types. The basics are covered in the typedState and structures guides:

Forcing types by msgID

As a quick recap,

  • Declare enum members with the message ID strings,
  • Export custom types for any structured packets
  • Add entries to the ElectricUIDeveloperState using the ID and the type

In the typedState.ts file,

export enum MSGID {
NICKNAME = 'name',
FAN = 'fan',
SUPERVISOR = 'sup',
}
declare global {
interface ElectricUIDeveloperState {
[MSGID.NICKNAME]: string
[MSGID.SUPERVISOR]: SupervisorState
[MSGID.FAN]:
| [FanSetting]
| [FanSetting, FanSetting] // if aux fan is supported
[MSGID.FAN_SPEED]: number | number[]
[MSGID.FAN_CURVE]: never // commented out in firmware
}
}
export type SupervisorState = {
control_state: string
fan_count: number
system_ok: boolean
}
export type FanSetting = {
rpm: number
setpoint: number
state: number
}

For projects with more than a handful of message identifiers, autocomplete suggestions are possible, instead of manually typing strings each time!

VSCode hints valid MSGID enum values

In conjunction, these basic language features allow for IDE completion suggestions with correct type information for the variable being used.

VSCode type hinting correctly displays the FanSetting members

Multi-typed variables

In situations where firmware might have compile-time options, or varies the number of array elements based on hardware configuration, it's useful to allow a set of types for a given message identifier.

This example allows one, two or three element arrays of FanSetting typed objects,

[MSGID.FAN]:
| [FanSetting]
| [FanSetting, FanSetting]
| [FanSetting, FanSetting, FanSetting]

The matching codec might look like this:

decode(payload: Buffer): FanSetting[] {
const reader = SmartBuffer.fromBuffer(payload)
const fanConfigs: FanSetting[] = []
while (reader.remaining() > 0) {
const fan: FanSetting = {
rpm: reader.readUInt8(),
setpoint: reader.readUInt8(),
state: reader.readUInt8() === 0x01 ? true : false,
}
fanConfigs.push(fan)
}
return fanConfigs
}

Handling dynamic arrays is covered in the Array example linked below, but the basic case is just accessing a hard-coded element.

If you attempt to access a non-existent 5th element of the array, the IDE will warn with a red underline and a type error is thrown at runtime.

<Printer accessor={state => state[MSGID.FAN][0].state}/>

The never type

The never type is used for any messageID which we know 'exists', but shouldn't be supported at all.

Unfortunately, the IDE completion isn't able to red-underline these, but it will throw a run-time error if it's encountered.