MFD Plugin¶
The MFD (Multi-Functional Display) plugin lets users build their own instrument panel by adding, removing, and configuring display blocks from the settings page. Each block can show different telemetry data, and the entire layout is persisted across sessions using QtCore -- so the UI a user creates survives app restarts.
It demonstrates advanced patterns including dynamic component loading, shared JavaScript modules, SVG assets, settings persistence of dynamic lists, and rich vehicle telemetry access.
What it does¶
- Users compose their own dashboard by adding MFD blocks from the settings panel.
- Three display types are available: textual (numeric readouts), graphical (compass with wind direction), and gauges (vertical bar gauges).
- Each block's type and data slots are configurable and persisted in settings, so the user's layout is restored on next launch.
- Reads extensive boat telemetry: speed, heading, course, wind, engine, fuel, gearbox, rudder, and more.
File structure¶
MFD/
├── plugin.json
├── build.sh
├── MFD.qrc ← Qt resource file
├── qmldir ← QML module definition
├── src/
│ ├── MFDData.qml ← data singleton
│ ├── MFDSettings.qml ← settings panel
│ ├── MFDUpperFlyView.qml ← instrument displays
│ └── Consts.js ← shared constants
└── img/
├── compass_needle_blue.svg
├── compassWind.svg
├── compassOuter.svg
└── expand.svg
Shared JavaScript: Consts.js¶
MFD uses a .pragma library JavaScript file to define shared constants. This pattern keeps magic numbers out of QML files and ensures consistency across all components.
.pragma library
const MFD_TYPES = Object.freeze({
TEXTUAL: 0,
GRAPHICAL: 1,
GAUGES: 2
});
const MFD_TYPE_LABELS = ["Textual", "Graphical", "Gauges"];
const DATA = Object.freeze({
NONE: 0, SPEED: 1, COURSE: 2, COG: 3, SOG: 4,
HEADING: 5, WIND_SPEED: 6, WIND_DIRECTION: 7,
ENGINE_STATE: 8, ENGINE_LOAD: 9, ENGINE_RPM: 10,
// ... and more
});
const DATA_LABELS = [
"Disabled", "Speed", "Course", "Course Over Ground",
"Speed Over Ground", "Heading", "Wind Spd", "Wind Dir",
// ... and more
];
const MFD_DATA_SLOTS = [
["Upper", "Lower"], // Textual
["Upper/Compass", "Lower", "Direction Arrow"], // Graphical
["Left", "Center", "Right"] // Gauges
];
Import it in QML files:
import "Consts.js" as Consts
// Use constants
text: Consts.DATA_LABELS[dataIndex]
visible: dataIndex !== Consts.DATA.NONE
MFDData.qml -- Data Singleton¶
The MFD data singleton manages a dynamic list of MFD instances using ListModel and persists them via Settings.
Key patterns¶
Vehicle telemetry access via the boat shorthand:
readonly property var boat: {
const vehicle = QGroundControl.multiVehicleManager.activeVehicle
if (vehicle && vehicle.boat) {
return vehicle
} else {
return null
}
}
Dynamic data lookup function:
function getDataValue(dataIndex) {
switch (dataIndex) {
case Consts.DATA.SPEED:
return boat && !isNaN(boat.groundSpeed.value)
? boat.groundSpeed.value.toFixed(1) : ""
case Consts.DATA.HEADING:
return boat && !isNaN(boat.heading.value)
? ((boat.heading.value + 360) % 360).toFixed(0) : ""
case Consts.DATA.SOG:
return boat && boat.sog
? QGroundControl.unitsConversion
.metersPerSecondToAppSettingsSpeedUnits(boat.sog)
.toFixed(1) : ""
case Consts.DATA.WIND_SPEED:
return boat && !isNaN(boat.wind.speed.value)
? boat.wind.speed.value : ""
case Consts.DATA.ENGINE_RPM:
return boat ? boat.engineRPM.toFixed(0) : ""
// ... more cases
}
}
Units lookup function:
function getDataUnits(dataIndex) {
switch (dataIndex) {
case Consts.DATA.SPEED:
return QGroundControl.unitsConversion.appSettingsSpeedUnitsString
case Consts.DATA.HEADING:
return "°"
case Consts.DATA.ENGINE_RPM:
return "RPM"
case Consts.DATA.ENGINE_TEMP:
return "°C"
case Consts.DATA.FUEL_LEVEL:
return "L"
// ...
}
}
ListModel management:
ListModel { id: mfdList; dynamicRoles: true }
function addMFD(type) {
mfdList.append({
"type": type,
"enabled": true,
"data1": getSlotDefault(type, 0),
"data2": getSlotDefault(type, 1),
"data3": getSlotDefault(type, 2)
})
saveMfdListToSettings()
}
function removeMFD(index) {
if (index >= 0 && index < mfdList.count) {
mfdList.remove(index)
saveMfdListToSettings()
}
}
MFDSettings.qml -- Settings Panel¶
The settings panel uses a Repeater to generate a configuration box for each MFD instance, plus buttons to add new MFDs of each type.
Key patterns:
QGCFlickablefor scrollable contentRepeateroverMFDData.mfdListwith per-item configurationQGCComboBoxfor data slot selection, writing back to the model- Dynamic "Add" buttons with count badges
// Per-MFD configuration
Repeater {
model: MFDData.mfdList.count
delegate: Rectangle {
readonly property int mfdIndex: modelData
readonly property var mfd: MFDData.mfdList.get(mfdIndex)
// Header: type label + delete button
RowLayout {
QGCLabel { text: Consts.MFD_TYPE_LABELS[mfd.type] }
QGCButton {
text: qsTr("Delete")
onClicked: MFDData.removeMFD(mfdIndex)
}
}
// Data slot combo boxes
Flow {
Repeater {
model: Consts.MFD_DATA_SLOTS[mfd.type].length
Column {
readonly property string keyName: "data" + (modelData + 1)
QGCLabel { text: Consts.MFD_DATA_SLOTS[mfd.type][modelData] }
QGCComboBox {
model: Consts.DATA_LABELS
currentIndex: mfd[keyName]
onActivated: (index) => {
MFDData.mfdList.setProperty(mfdIndex, keyName, index)
MFDData.saveMfdListToSettings()
}
}
}
}
}
}
}
MFDUpperFlyView.qml -- Instrument Displays¶
The upper fly view uses a Loader per MFD to dynamically instantiate the correct component based on type.
Dynamic component loading¶
Repeater {
model: MFDData.mfdList
delegate: Loader {
readonly property var thisMFD: model
readonly property int data1: thisMFD ? thisMFD.data1 : 0
readonly property int data2: thisMFD ? thisMFD.data2 : 0
readonly property int data3: thisMFD ? thisMFD.data3 : 0
sourceComponent: {
switch (model.type) {
case Consts.MFD_TYPES.TEXTUAL:
return textualMFDComponent
case Consts.MFD_TYPES.GRAPHICAL:
return graphicalMFDComponent
case Consts.MFD_TYPES.GAUGES:
return gaugesMFDComponent
}
}
}
}
Textual MFD¶
Displays two numeric values with labels and units in a bordered rectangle:
Component {
id: textualMFDComponent
Rectangle {
width: mfdSideLength
height: width
color: qgcPal.windowShadeDark
border.color: qgcPal.colorRed
// Upper data: label + value + units
QGCLabel { text: Consts.DATA_LABELS[data1] }
QGCLabel { text: MFDData.getDataValue(data1) }
QGCLabel { text: MFDData.getDataUnits(data1) }
// Lower data: value + units + label
QGCLabel { text: MFDData.getDataValue(data2) }
QGCLabel { text: MFDData.getDataUnits(data2) }
QGCLabel { text: Consts.DATA_LABELS[data2] }
}
}
Graphical MFD (Compass)¶
Displays a rotating compass with a directional arrow and data overlays:
- Compass ring rotates based on heading data
- Wind direction arrow drawn with
QtQuick.Shapes - Vehicle icon overlay using
QGCColoredImagewith SVG - Horizontal rudder gauge at the bottom
Component {
id: graphicalMFDComponent
Rectangle {
readonly property real compassHeading:
Number(MFDData.getDataValue(data1)) * -1
// Rotating compass ring
Rectangle {
rotation: compassHeading
// N/S/E/W labels, inner ring
}
// Wind direction arrow using Shape + ShapePath
Shape {
ShapePath {
fillColor: "transparent"
strokeColor: qgcPal.colorBlue
// Arrow geometry
}
}
// Vehicle icon
QGCColoredImage {
source: "/images/compass_needle_blue.svg"
color: qgcPal.colorGreen
}
}
}
Gauges MFD¶
Displays 1-3 vertical bar gauges with auto-scaling min/max:
Component {
id: gaugesMFDComponent
Rectangle {
RowLayout {
Repeater {
model: Consts.MAX_GAUGES
delegate: ColumnLayout {
readonly property real value:
Number(MFDData.getDataValue(dataIndex))
property real valueMin: 0
property real valueMax: 1
onValueChanged: {
valueMin = Math.min(value, valueMin)
valueMax = Math.max(value, valueMax)
}
// Label, vertical bar, min/max/units
}
}
}
}
}
Telemetry accessed¶
The MFD plugin reads the following vehicle properties:
| Data | Access path |
|---|---|
| Ground speed | vehicle.groundSpeed.value |
| Heading | vehicle.heading.value |
| Course | vehicle.usvCourse |
| Course over ground | vehicle.gps.courseOverGround.value |
| Speed over ground | vehicle.sog |
| Wind speed | vehicle.wind.speed.value |
| Wind direction | vehicle.wind.direction.value |
| Engine state | vehicle.engineState |
| Engine load | vehicle.engineLoad |
| Engine RPM | vehicle.engineRPM |
| Gearbox state | vehicle.gearboxState |
| Gearbox position | vehicle.gearboxPos |
| Engine temp | vehicle.engineTemp |
| Fuel level | vehicle.fuel.level |
| Fuel max | vehicle.fuel.maximum |
| Fuel consumed | vehicle.fuel.consumed |
| Fuel rate | vehicle.fuel.consumptionRate |
| Trim position | vehicle.engineTrimPos |
| Trim state | vehicle.engineTrimState |
| Throttle | vehicle.throttlePos |
| Rudder position | vehicle.rudderPos |
| Rudder state | vehicle.rudderState |
| Oil pressure | vehicle.oilPressure |