Connection card customisation

Use MetadataRequest to fetch and display firmware build information on the connections page Connections cards.

Screenshot of component Connections build-info

Firmware Version packet

Hardware will provide build information with a custom typed packet including the build time, git status, and versioned tag name.

There are different ways to embed git metadata into the binary, most common are generating a header file during the build process with a bash script or with CMake, or sometimes injected via CFLAGS as part of the CI/CD pipeline.

The eUI tracked custom type used for this example is a structure of 12-character arrays.

typedef struct
{
char build_branch[12];
char build_info[12];
char build_date[12];
char build_time[12];
char build_type[12];
char build_name[12];
} BuildInfo_t;

A matching codec implementation is described below,

  • Split the inbound packet into 12-byte chunks using splitBufferByLength()
  • Read the null-terminated string out of each chunk
  • Return the FirmwareBuildInfo object populated with the strings
const build_info_bytes: number = 12
export function splitBufferByLength(toSplit: Buffer, splitLength: number) {
const chunks = []
const n = toSplit.length
let i = 0
// if the result is only going to be one chunk, just return immediately.
if (toSplit.length < splitLength) {
return [toSplit]
}
while (i < n) {
let end = i + splitLength
chunks.push(toSplit.slice(i, end))
i = end
}
return chunks
}
export class FirmwareInfoCodec extends Codec {
filter(message: Message): boolean {
return message.messageID === MSGID.FIRMWARE_INFO
}
encode(payload: FirmwareBuildInfo): Buffer {
throw new Error('Firmware/build info is read-only')
}
decode(payload: Buffer): FirmwareBuildInfo {
const chunks = splitBufferByLength(payload, build_info_bytes)
const strings = chunks.map(chunk =>
SmartBuffer.fromBuffer(chunk).readStringNT(),
)
return {
branch: strings[0],
info: strings[1],
date: strings[2],
time: strings[3],
type: strings[4],
name: strings[5],
}
}
}

Once setup, this information can be viewed in the Device State view after the connection handshake occurs.

Device State view of decoded version info packet

Now the version information is available, lets display it on the connections card before the handshake.

Metadata Requests

The interface template generated by arc already includes a metadata request feature for the name message ID in /src/transport-manager/config/metadata.tsx.

Either modify it, or create a new file. The key changes are requesting the fwb message ID and putting the processed payload into the firmware_info member of the device metadata.

import {
CancellationToken,
Device,
DiscoveryMetadataProcessor,
DiscoveryMetadataRequester,
FoundHint,
Hint,
Message,
} from '@electricui/core'
// Request and Process the firmware info 'fwb' struct for every device
class RequestBuild extends DiscoveryMetadataRequester {
name = 'request-build'
canRequestMetadata(device: Device) {
return true
}
requestMetadata(device: Device) {
const nameRequest = new Message('fwb', null)
nameRequest.metadata.query = true
nameRequest.metadata.internal = false
const cancellationToken = new CancellationToken(
'request firmware build info metadata',
).deadline(1_000)
return device
.write(nameRequest, cancellationToken)
.then(res => {
console.log('Requested fwb, response:', res)
})
.catch(err => {
console.log("Couldn't request fwb err:", err)
})
}
}
class ProcessBuild extends DiscoveryMetadataProcessor {
isRelevantMessage(message: Message, device: Device) {
// if this is an ack packet, ignore it
if (message.metadata.ackNum > 0 && message.payload === null) {
return false
}
// if it's a firmware version packet, process it
if (message.messageID === 'fwb') {
return true
}
return false
}
processMetadata(message: Message, device: Device, foundHint: FoundHint) {
if (message.messageID === 'fwb') {
device.addMetadata({
firmware_info: message.payload
})
}
}
}
export { RequestBuild, ProcessBuild }

In /src/transport-manager/config/index.tsx, ensure RequestBuild and ProcessBuild are imported, and new objects are created, then added to the deviceManager.

const requestBuild = new RequestBuild()
const processBuild = new ProcessBuild()
deviceManager.addDeviceMetadataRequesters([requestName, requestBuild])
deviceManager.addDiscoveryMetadataProcessors([processName, processBuild])

Feel free to remove requestName and processName if they aren't needed!

Connection Card Customisation

In /src/application/pages/ConnectionPage.tsx, the Connections component is responsible for populating the list of viable hardware devices. The internalCardComponent property allows us to provide custom content for each device.

<Connections
preConnect={deviceID => navigate(`/device_loading/${deviceID}`)}
postHandshake={deviceID => navigate(`/devices/${deviceID}`)}
onFailure={(deviceID, err) => {
console.log('Connections component got error', err, deviceID)
navigate(`/`)
}}
internalCardComponent={<h3>A Device!</h3>}
/>
Screenshot of component Connections basic-h3

We'll create a custom layout in a separate component CardInternals which grabs the firmware information using the useDeviceMetadataKey hook before displaying it.

The metadata key 'firmware_info' needs to match the key set in the processMetadata() processor earlier.

Screenshot of component Connections build-info
const CardInternals = () => {
const firmwareInfo = useDeviceMetadataKey('firmware_info')
if (!firmwareInfo) {
return <h3 className={Classes.HEADING}>No build info</h3>
}
return (
<React.Fragment>
<h3 className={Classes.HEADING}>{firmwareInfo.name}</h3>
<div style={{ opacity: '0.5', fontSize: 'small' }}>
<b>{firmwareInfo.info}</b> on <b>{firmwareInfo.branch}</b>
</div>
<div style={{ opacity: '0.5', fontSize: 'smaller' }}>
{firmwareInfo.date} {firmwareInfo.time}
</div>
</React.Fragment>
)
}

Remember to use the component on the card:

internalCardComponent={<CardInternals/>}

This same approach can be used to display other device information on the connection card - battery percentages or end-user settable names are other common information to display on the card.