Skip to content

Commit 5988dfe

Browse files
settings: use shared platform catalog and save craft initial configuration
1 parent d0dc0c2 commit 5988dfe

17 files changed

Lines changed: 1629 additions & 352 deletions

‎doc/openhd_settings_map.json‎

Lines changed: 1146 additions & 161 deletions
Large diffs are not rendered by default.

‎doc/shared-settings.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Initial configuration and FleetControl
2+
3+
Select a craft in FleetControl, configure its image options, then use **Save
4+
configuration** to save a version-1 initial snapshot on that craft. **Export JSON**
5+
saves the initial `settings.json` locally. The snapshot uses the image-writing
6+
builder and excludes FleetControl login credentials and enrollment tokens.
7+
8+
Selecting the craft again restores the editable settings and typed
9+
`parameter_defaults`. A profile without a plan clears the previous craft's plan.
10+
Open initial-default controls follow profile changes; Ground removes camera
11+
component defaults. OpenHD seeds new persistent owners only, preserving existing
12+
device preferences.
13+
14+
Hardware selectors use generated platform IDs and labels from OpenHD's
15+
`settings/platforms.json`. The online craft editor refreshes the platform catalog
16+
from `/api/imagewriter/platforms`; offline use retains the bundled list. Combined
17+
legacy board names require a concrete selection. Image availability and flashing
18+
transport still depend on the selected image.
19+
20+
FleetControl's Hangar editor shows and downloads the captured initial file.
21+
Ordinary craft edits preserve that snapshot until it is explicitly saved again.
22+
The full contract is documented in OpenHD's `OpenHD/settings/README.md`.
23+
24+
Tests: `node src/test/fleet_profiles_test.js`, `node src/test/catalog_test.js`,
25+
`node src/test/fleetcontrol_catalog_test.js`, and the CMake targets
26+
`openhd_backend_tests` / `initialsettings_qml_test` under `src/test`.

‎src/ImageOptionsPage.qml‎

Lines changed: 46 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Qt.labs.settings 1.0
1111
import QtQuick.Dialogs 1.3
1212
import "qmlcomponents"
1313
import "qmlcomponents/SettingsLabels.js" as SettingsLabels
14+
import "qmlcomponents/FleetProfilesHelper.js" as FleetProfilesHelper
1415

1516
Rectangle {
1617
id: page
@@ -29,10 +30,14 @@ Rectangle {
2930

3031
property var settingsMap: ({})
3132
property bool settingsMapLoaded: false
33+
onVisibleChanged: {
34+
if (visible && settingsMapLoaded) settingsMap = JSON.parse(JSON.stringify(FleetProfilesHelper.getSettingsMap(imageWriter)))
35+
}
3236

3337
// refactored settings
3438
property string bootType
3539
property string fileName
40+
property string platform: imageWriter.getValue("platform") || ""
3641
property string sbc
3742
property string camera
3843
property string camera2
@@ -165,6 +170,33 @@ Rectangle {
165170
width: deviceRoleCard.availableWidth
166171
spacing: 10
167172

173+
Label { text: qsTr("Hardware platform"); Layout.fillWidth: true }
174+
ModernComboBox {
175+
Layout.fillWidth: true
176+
model: settingsMap.platforms || []
177+
textRole: "label"
178+
currentIndex: {
179+
for (var n=0; n<model.length; ++n) if (model[n].id === page.platform) return n
180+
return -1
181+
}
182+
displayText: currentIndex >= 0 ? currentText : qsTr("Choose hardware platform")
183+
onActivated: {
184+
var board = model[currentIndex]
185+
page.platform = board.id
186+
sbc = board.sbc
187+
imageWriter.setSetting("platform", board.id)
188+
imageWriter.setSetting("sbc", board.sbc)
189+
if (board.roles.indexOf(bootType.toLowerCase()) < 0) bootType = board.roles[0] === "air" ? "Air" : "Ground"
190+
rpi = sbc === "rpi"; rock5 = sbc === "rock-5a" || sbc === "rock-5b" || sbc === "radxa-cm5"; rock3 = sbc === "zero3w" || sbc === "radxa-cm3"; page.refreshCameraOptions()
191+
}
192+
}
193+
InitialSettingsPanel {
194+
id: initialDefaults
195+
Layout.fillWidth: true
196+
settingsOwner: imageWriter
197+
fields: settingsMap.initialFields || []
198+
role: String(bootType).toLowerCase()
199+
}
168200
ModernComboBox {
169201
id: bootTypeSelector
170202
Layout.fillWidth: true
@@ -1074,86 +1106,15 @@ Rectangle {
10741106
else if (!supportsAir)
10751107
bootType = "Ground"
10761108

1077-
if (normalizedPlatform.indexOf("raspberry-pi") === 0) {
1078-
imageWriter.setSetting("sbc", "rpi");
1079-
sbc = "rpi"
1080-
rpi=true;
1081-
rock5=false;
1082-
rock3=false;
1083-
}
1084-
else if (normalizedFileName.includes("rock5a") || normalizedFileName.includes("rock-5a")) {
1085-
imageWriter.setSetting("sbc", "rock-5a");
1086-
sbc = "rock-5a"
1087-
rpi=false; rock5=true; rock3=false;
1088-
}
1089-
else if (normalizedFileName.includes("rock5b") || normalizedFileName.includes("rock-5b")) {
1090-
imageWriter.setSetting("sbc", "rock-5b");
1091-
sbc = "rock-5b"
1092-
rpi=false; rock5=true; rock3=false;
1093-
}
1094-
else if (normalizedFileName.includes("zero3") || normalizedFileName.includes("zero-3")) {
1095-
imageWriter.setSetting("sbc", "zero3w");
1096-
sbc = "zero3w"
1097-
rpi=false; rock5=false; rock3=true;
1098-
}
1099-
else if (normalizedFileName.includes("radxa-cm5") || normalizedFileName.includes("radxa_cm5")) {
1100-
sbc = "radxa-cm5"; rpi=false; rock5=true; rock3=false;
1101-
}
1102-
else if (normalizedFileName.includes("radxa-cm3") || normalizedFileName.includes("radxa_cm3")) {
1103-
imageWriter.setSetting("sbc", "radxa-cm3");
1104-
sbc = "radxa-cm3"
1105-
rpi=false; rock5=false; rock3=true;
1106-
}
1107-
else if (normalizedFileName.includes("qrb5165")) {
1108-
sbc = "qrb5165"; rpi=false; rock5=false; rock3=false;
1109-
}
1110-
else if (normalizedFileName.includes("qcs405")) {
1111-
sbc = "qcs405"; rpi=false; rock5=false; rock3=false;
1112-
}
1113-
else if (normalizedFileName.includes("openipc")) {
1114-
sbc = "openipc"; rpi=false; rock5=false; rock3=false;
1115-
}
1116-
else if (normalizedFileName.includes("rv1103") || normalizedFileName.includes("rv1106") || normalizedFileName.includes("rv1126") || normalizedFileName.includes("luckfox")) {
1117-
sbc = "rockchip-rv"; rpi=false; rock5=false; rock3=false;
1118-
}
1119-
else if (normalizedFileName.includes("orqa")) {
1120-
sbc = "orqa"; rpi=false; rock5=false; rock3=false;
1121-
}
1122-
else if (normalizedFileName.includes("x20")) {
1123-
sbc = "x20"; rpi=false; rock5=false; rock3=false;
1124-
}
1125-
else if (normalizedFileName.includes("x21")) {
1126-
sbc = "x21"; rpi=false; rock5=false; rock3=false;
1127-
}
1128-
else if (normalizedFileName.includes("cubie") || normalizedFileName.includes("a733")) {
1129-
sbc = "cubie"; rpi=false; rock5=false; rock3=false;
1130-
}
1131-
else if (normalizedFileName.includes("xavier") || normalizedFileName.includes("jetson")) {
1132-
sbc = "nvidia-xavier"; rpi=false; rock5=false; rock3=false;
1133-
}
1134-
else if (normalizedFileName.includes("uvx") || normalizedFileName.includes("uvx-mod")) {
1135-
sbc = "uvx"; rpi=false; rock5=false; rock3=false;
1136-
}
1137-
else if (normalizedFileName.includes("imx8") || normalizedFileName.includes("nxp")) {
1138-
sbc = "nxp-imx8"; rpi=false; rock5=false; rock3=false;
1139-
}
1140-
else if (normalizedFileName.includes("x86") || normalizedFileName.includes("amd64")) {
1141-
sbc = "x86"; rpi=false; rock5=false; rock3=false;
1142-
}
1143-
else if (normalizedFileName.includes("raspberry") || normalizedFileName.includes("rpi") || normalizedFileName.includes("image-pi") || normalizedFileName.includes("image_pi")) {
1144-
imageWriter.setSetting("sbc", "rpi");
1145-
sbc = "rpi"
1146-
rpi=true;
1147-
rock5=false;
1148-
rock3=false;
1149-
}
1150-
else{
1151-
imageWriter.setSetting("sbc", "unknown");
1152-
sbc = "unknown"
1153-
rpi=false;
1154-
rock5=false;
1155-
rock3=false;
1156-
}
1109+
FleetProfilesHelper.getSettingsMap(imageWriter)
1110+
var board = FleetProfilesHelper.platformForHint(normalizedPlatform) || FleetProfilesHelper.platformForHint(normalizedFileName) ||
1111+
FleetProfilesHelper.platformForHardware(imageWriter.getValue("platform"))
1112+
platform = board ? board.id : ""
1113+
sbc = board ? board.sbc : "unknown"
1114+
rpi = sbc === "rpi"
1115+
rock5 = sbc === "rock-5a" || sbc === "rock-5b" || sbc === "radxa-cm5"
1116+
rock3 = sbc === "zero3w" || sbc === "radxa-cm3"
1117+
imageWriter.setSetting("platform", platform)
11571118
imageWriter.setSetting("sbc", sbc)
11581119
imageWriter.setSetting("bootType", bootType)
11591120

@@ -1241,8 +1202,10 @@ Rectangle {
12411202
function availableBootOptions() {
12421203
if (!settingsMap.bootType || !settingsMap.bootType.options)
12431204
return []
1205+
var board = FleetProfilesHelper.platformForHardware(platform)
12441206
return settingsMap.bootType.options.filter(function(option) {
12451207
return option.id && option.id.length > 0 &&
1208+
(!board || board.roles.indexOf(option.id.toLowerCase()) >= 0) &&
12461209
((option.id === "Air" && supportsAir) ||
12471210
(option.id === "Ground" && supportsGround))
12481211
})
@@ -1269,6 +1232,8 @@ Rectangle {
12691232
}
12701233
}
12711234

1235+
imageWriter.setSetting("platform", platform)
1236+
imageWriter.setSetting("initialParameterDefaults", JSON.stringify(initialDefaults.exportDefaults()))
12721237
imageWriter.setSetting("bootType", bootType)
12731238
imageWriter.setSetting("camera", camera)
12741239
imageWriter.setSetting("camera2", camera2)
@@ -1323,7 +1288,7 @@ Rectangle {
13231288
return
13241289

13251290
try {
1326-
settingsMap = JSON.parse(imageWriter.readResourceText(":/doc/openhd_settings_map.json"))
1291+
settingsMap = FleetProfilesHelper.getSettingsMap(imageWriter)
13271292
settingsMapLoaded = true
13281293
} catch (e) {
13291294
console.log("Failed to load OpenHD settings map: " + e)

‎src/configure.qml‎

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ Rectangle {
3535
// Settings map and values shared with the image configuration page.
3636
property var settingsMap: ({})
3737
property bool settingsMapLoaded: false
38+
property string settingsMapSourceRoot: ""
39+
property bool hasImageSettingsManifest: false
3840
property string bootType: ""
41+
property string platform: imageWriter.getValue("platform") || ""
3942
property string sbc: ""
4043
property string camera: ""
4144
property string camera2: ""
@@ -66,6 +69,11 @@ Rectangle {
6669
property bool returnHomeAfterPopupClose: false
6770
property string language: ""
6871
property string token: ""
72+
function platformSupportsRole(role) {
73+
var boards = settingsMap.platforms || []
74+
for (var n=0; n<boards.length; ++n) if (boards[n].id === platform) return boards[n].roles.indexOf(role.toLowerCase()) >= 0
75+
return true
76+
}
6977

7078
Component.onCompleted: {
7179
qopenhdConfPath = normalizeLocalFilePath(imageWriter.getValue("qopenhdConfPath"))
@@ -376,17 +384,44 @@ ImButton {
376384
}
377385
ColumnLayout {
378386
id: settingsBody
387+
InitialSettingsPanel {
388+
id: initialDefaults
389+
settingsOwner: imageWriter
390+
Layout.fillWidth: true
391+
fields: hasImageSettingsManifest ? (settingsMap.initialFields || []) : []
392+
role: bootType.toLowerCase()
393+
}
379394
width: settingsScroll.width - settingsScroll.leftPadding - settingsScroll.rightPadding
380395
spacing: 16
381396

397+
Label { text: qsTr("Hardware platform"); Layout.fillWidth: true }
398+
ModernComboBox {
399+
Layout.fillWidth: true
400+
model: settingsMap.platforms || []
401+
textRole: "label"
402+
currentIndex: {
403+
for (var n=0; n<model.length; ++n) if (model[n].id === window.platform) return n
404+
return -1
405+
}
406+
displayText: currentIndex >= 0 ? currentText : qsTr("Choose hardware platform")
407+
onActivated: {
408+
var board = model[currentIndex]
409+
window.platform = board.id
410+
sbc = board.sbc
411+
imageWriter.setSetting("platform", board.id)
412+
imageWriter.setSetting("sbc", board.sbc)
413+
if (board.roles.indexOf(bootType.toLowerCase()) < 0) bootType = board.roles[0] === "air" ? "Air" : "Ground"
414+
rebuildCameraSelectors()
415+
}
416+
}
382417
SettingsSection {
383418
title: qsTr("Boot Mode")
384419
Layout.fillWidth: true
385420

386421
ColumnLayout {
387422
spacing: 0
388423
Repeater {
389-
model: settingsMap.bootType && settingsMap.bootType.options ? settingsMap.bootType.options.filter(function(option) { return option.id && option.id.length > 0 }) : []
424+
model: settingsMap.bootType && settingsMap.bootType.options ? settingsMap.bootType.options.filter(function(option) { return option.id && option.id.length > 0 && platformSupportsRole(option.id) }) : []
390425
delegate: ImCheckBox {
391426
property var option: modelData
392427
text: option ? SettingsLabels.translated(option.id) : ""
@@ -576,7 +611,7 @@ ImButton {
576611
}
577612

578613
var targetIndex = 0
579-
if (cameraResolution && cameraResolution.length > 0) {
614+
if (cameraResolution && cameraResolution.length > 0) {
580615
for (var idx = 0; idx < cameraResolutionOptionsModel.count; idx++) {
581616
if (cameraResolutionOptionsModel.get(idx).value === cameraResolution) {
582617
targetIndex = idx
@@ -1299,11 +1334,19 @@ ImButton {
12991334
}
13001335

13011336
function loadSettingsMap() {
1302-
if (settingsMapLoaded)
1337+
if (settingsMapLoaded && settingsMapSourceRoot === openhdRoot)
13031338
return
13041339

13051340
try {
1306-
settingsMap = JSON.parse(imageWriter.readResourceText(":/doc/openhd_settings_map.json"))
1341+
var manifestText = openhdRoot ? imageWriter.readTextFile(drivePath("settings-manifest.json")) : ""
1342+
hasImageSettingsManifest = manifestText.length > 0
1343+
settingsMap = JSON.parse(hasImageSettingsManifest ? manifestText : imageWriter.readResourceText(":/doc/openhd_settings_map.json"))
1344+
if (hasImageSettingsManifest && (!settingsMap._settingsContract || settingsMap._settingsContract.version !== 1)) {
1345+
settingsMap = {}
1346+
hasImageSettingsManifest = false
1347+
throw new Error("Unsupported image settings manifest")
1348+
}
1349+
settingsMapSourceRoot = openhdRoot
13071350
settingsMapLoaded = true
13081351
} catch (e) {
13091352
console.log("[Configure] Failed to load OpenHD settings map: " + e)
@@ -1470,9 +1513,11 @@ ImButton {
14701513

14711514
var settingsJson = imageWriter.readTextFile(drivePath("settings.json"))
14721515
var settingsObj = {}
1516+
if (platform) settingsObj.platform = platform
14731517
if (settingsJson && settingsJson.length > 0) {
14741518
try {
14751519
settingsObj = JSON.parse(settingsJson)
1520+
initialDefaults.defaults = settingsObj.parameter_defaults || {version: 1, components: {}}
14761521
} catch (e) {
14771522
console.log("[Configure] Error parsing settings.json:", e)
14781523
}
@@ -1491,6 +1536,7 @@ ImButton {
14911536
} else if (settingsMap.sbc && settingsMap.sbc.options && settingsMap.sbc.options.length > 0) {
14921537
sbc = settingsMap.sbc.options[0].id
14931538
}
1539+
platform = settingsObj.platform || ""
14941540

14951541
if (settingsObj.debug) {
14961542
mode = "debug"
@@ -1597,6 +1643,7 @@ ImButton {
15971643
console.log("[Configure] Writing settings to", openhdRoot)
15981644

15991645
var settingsObj = {}
1646+
settingsObj.parameter_defaults = initialDefaults.exportDefaults()
16001647
if (bootType === "Air") {
16011648
settingsObj.role = "air"
16021649
} else if (bootType === "Ground") {
@@ -1709,11 +1756,13 @@ ImButton {
17091756
premiumCertificatePresent = imageWriter.fileExists(drivePath(premiumCertificateRelativePath()))
17101757
}
17111758

1759+
imageWriter.setSetting("platform", platform)
17121760
imageWriter.setSetting("bootType", bootType)
17131761
imageWriter.setSetting("sbc", sbc)
17141762
imageWriter.setSetting("camera", camera)
17151763
imageWriter.setSetting("camera2", camera2)
17161764
imageWriter.setSetting("cameraResolution", cameraResolution)
1765+
imageWriter.setSetting("initialParameterDefaults", JSON.stringify(initialDefaults.exportDefaults()))
17171766
imageWriter.setSetting("camera2Resolution", camera2Resolution)
17181767
imageWriter.setSetting("cameraPort", cameraPort)
17191768
imageWriter.setSetting("camera2Port", camera2Port)

‎src/imagewriter.cpp‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,9 +1635,15 @@ void ImageWriter::setSetting(const QString &key, const QVariant &value)
16351635

16361636
_settings.setValue(key, value);
16371637
_settings.sync();
1638+
emit settingChanged(key, value);
16381639

16391640
}
16401641

1642+
QVariantMap ImageWriter::initialConfiguration() const
1643+
{
1644+
return OpenHDImageCustomizer::initialConfiguration(_settings).toVariantMap();
1645+
}
1646+
16411647
QString ImageWriter::crypt(const QByteArray &password)
16421648
{
16431649
QByteArray salt = "$5$";

‎src/imagewriter.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ class ImageWriter : public QObject
166166
Q_INVOKABLE bool getBoolSetting(const QString &key);
167167
Q_INVOKABLE QString getValue(const QString &key);
168168
Q_INVOKABLE void setSetting(const QString &key, const QVariant &value);
169+
Q_INVOKABLE QVariantMap initialConfiguration() const;
169170
Q_INVOKABLE QVariantMap getSavedCustomizationSettings();
170171
Q_INVOKABLE void clearSavedCustomizationSettings();
171172
Q_INVOKABLE bool hasSavedCustomizationSettings();
@@ -190,6 +191,7 @@ class ImageWriter : public QObject
190191
Q_INVOKABLE bool hasMouse();
191192

192193
signals:
194+
void settingChanged(const QString &key, const QVariant &value);
193195
/* We are emiting signals with QVariant as parameters because QML likes it that way */
194196

195197
void downloadProgress(QVariant dlnow, QVariant dltotal);

0 commit comments

Comments
 (0)