Connection card customisation
Use MetadataRequest to fetch and display firmware build information on the connections page Connections
cards.
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 = 12export 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.
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 deviceclass 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>} />
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 theprocessMetadata()
processor earlier.
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.